From bd0511ddb36a64bd2e16152bd050520f0ba1aa4a Mon Sep 17 00:00:00 2001 From: ubeuu Date: Wed, 14 Aug 2024 05:07:41 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:s3=EC=99=80=20cloudfront=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=B4=EC=84=9C=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=8D=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 +-- build.gradle.kts | 3 + .../ward_server/global/config/S3Config.java | 34 +++++++++ .../global/config/S3MockConfig.java | 49 +++++++++++++ .../global/util/S3ImageUploader.java | 65 +++++++++++++++++ .../global/util/S3ImageUploaderTest.java | 68 ++++++++++++++++++ src/test/resources/test-image.png | Bin 0 -> 12119 bytes 7 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/ward/ward_server/global/config/S3Config.java create mode 100644 src/main/java/com/ward/ward_server/global/config/S3MockConfig.java create mode 100644 src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java create mode 100644 src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java create mode 100644 src/test/resources/test-image.png diff --git a/.gitignore b/.gitignore index 490daff..0722098 100644 --- a/.gitignore +++ b/.gitignore @@ -326,9 +326,6 @@ gradle-app.setting # End of https://www.toptal.com/developers/gitignore/api/intellij,gradle,java,windows,macos,intellij+all -# ----- 하늘님 gitignore 추가 요청 20231204.1641 -.DS_store - #yml *.yml !application.yml @@ -347,4 +344,7 @@ gradlew.bat gradlew # QueryDSL Q-classes -src/main/generated/ \ No newline at end of file +src/main/generated/ + +# s3 test folder +s3mock \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index aac06de..a3efb30 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,9 +55,12 @@ dependencies { implementation("io.jsonwebtoken:jjwt-api:0.11.5") implementation("io.jsonwebtoken:jjwt-impl:0.11.5") implementation("io.jsonwebtoken:jjwt-jackson:0.11.5") + //s3 + implementation("io.awspring.cloud:spring-cloud-starter-aws:2.4.4") //test testImplementation ("org.springframework.boot:spring-boot-starter-test") testRuntimeOnly ("com.h2database:h2") + implementation ("io.findify:s3mock_2.13:0.2.6") } tasks.withType { diff --git a/src/main/java/com/ward/ward_server/global/config/S3Config.java b/src/main/java/com/ward/ward_server/global/config/S3Config.java new file mode 100644 index 0000000..4cc6940 --- /dev/null +++ b/src/main/java/com/ward/ward_server/global/config/S3Config.java @@ -0,0 +1,34 @@ +package com.ward.ward_server.global.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Slf4j +public class S3Config { + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3() { + //fixme debug로 고치기 + log.info("하늘:" + accessKey); + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .withRegion(region) + .build(); + } +} diff --git a/src/main/java/com/ward/ward_server/global/config/S3MockConfig.java b/src/main/java/com/ward/ward_server/global/config/S3MockConfig.java new file mode 100644 index 0000000..79599ec --- /dev/null +++ b/src/main/java/com/ward/ward_server/global/config/S3MockConfig.java @@ -0,0 +1,49 @@ +package com.ward.ward_server.global.config; + +import akka.http.scaladsl.Http; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.AnonymousAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import io.findify.s3mock.S3Mock; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Profile; + +@Slf4j +@Profile("test") +@Configuration +public class S3MockConfig { + private int port; + + @Bean(name = "s3Mock") + public S3Mock s3Mock() { + log.info("s3 mock 빈 생성"); //fixme debug로 고치기 + S3Mock s3Mock = S3Mock.create(0, "s3mock"); + Http.ServerBinding binding = s3Mock.start(); + port = binding.localAddress().getPort(); + log.info("port:{}", port); + return s3Mock; + } + + @Bean + @DependsOn("s3Mock") + public AmazonS3Client amazonS3() { + log.info("amazon client 빈 생성"); + AwsClientBuilder.EndpointConfiguration endpoint = + new AwsClientBuilder.EndpointConfiguration( + "http://127.0.0.1:" + port, Regions.AP_NORTHEAST_2.name()); + AmazonS3Client client = (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withPathStyleAccessEnabled(true) + .withEndpointConfiguration(endpoint) + .withCredentials(new AWSStaticCredentialsProvider(new AnonymousAWSCredentials())) + .build(); + client.createBucket("mock-bucket"); + return client; + } +} diff --git a/src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java b/src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java new file mode 100644 index 0000000..a64af9c --- /dev/null +++ b/src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java @@ -0,0 +1,65 @@ +package com.ward.ward_server.global.util; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.ward.ward_server.global.exception.ApiException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +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.Optional; + +import static com.ward.ward_server.global.exception.ExceptionCode.INVALID_INPUT; +import static com.ward.ward_server.global.response.error.ErrorMessage.FILE_CONVERT_FAIL; + +@RequiredArgsConstructor +@Component +@Slf4j +public class S3ImageUploader { + private final AmazonS3Client amazonS3Client; + @Value("${cloud.aws.s3.bucket}") + private String bucket; + @Value("${cloud.aws.cloudFront.domain}") + private String cloudFrontDomain; + + public String upload(MultipartFile multipartFile, String dirName) throws IOException { + File uploadFile = convert(multipartFile) + .orElseThrow(() -> new ApiException(INVALID_INPUT, FILE_CONVERT_FAIL.getMessage())); + log.info("uploadFile name: {}", uploadFile.getName()); //fixme debuf로 고치기 + String fileName = dirName + "/" + uploadFile.getName(); + String uploadImageUrl = putS3(uploadFile, fileName); + uploadFile.delete(); + return uploadImageUrl; + } + + private String putS3(File uploadFile, String fileName) { + amazonS3Client.putObject( + new PutObjectRequest(bucket, fileName, uploadFile) + .withCannedAcl(CannedAccessControlList.PublicRead) + ); + return cloudFrontDomain + "/" + fileName; + } + + public String getUrl(String bucket, String fileName) { + return amazonS3Client.getUrl(bucket, fileName).toString(); + } + + private Optional convert(MultipartFile file) throws IOException { + File convertFile = new File(file.getOriginalFilename()); + log.info("convertFile name: {}", convertFile.getName()); + if (convertFile.createNewFile()) { + log.info("create new file"); + try (FileOutputStream fos = new FileOutputStream(convertFile)) { + fos.write(file.getBytes()); + } + return Optional.of(convertFile); + } + return Optional.empty(); + } +} diff --git a/src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java b/src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java new file mode 100644 index 0000000..6f90ec7 --- /dev/null +++ b/src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java @@ -0,0 +1,68 @@ +package com.ward.ward_server.global.util; + +import com.ward.ward_server.global.config.S3MockConfig; +import io.findify.s3mock.S3Mock; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +@Slf4j +@ActiveProfiles("test") +@ExtendWith(SpringExtension.class) +@Import(S3MockConfig.class) +@ContextConfiguration(classes = S3ImageUploader.class) +class S3ImageUploaderTest { + + + @Autowired + private S3Mock s3Mock; + @Autowired + private S3ImageUploader s3ImageUploader; + + @AfterEach + public void tearDown() { + log.debug("s3mock server stop"); + s3Mock.stop(); + } + + @Test + void 이미지_업로드() throws IOException { + // given + String dirName = "main"; + String mockBucketName = "mock-bucket"; + String mockCloudFrontDomain = "https://mock.cloudfront.net"; + File file = ResourceUtils.getFile("classpath:test-image.png"); + MultipartFile multipartFile = new MockMultipartFile( + "test", + file.getName(), + "image/png", + new FileInputStream(file) + ); + ReflectionTestUtils.setField(s3ImageUploader, "bucket", mockBucketName); + ReflectionTestUtils.setField(s3ImageUploader, "cloudFrontDomain", mockCloudFrontDomain); + + // when + String urlPath = s3ImageUploader.upload(multipartFile, dirName); + + // then + assertThat(urlPath).contains(mockCloudFrontDomain); + assertThat(urlPath).contains(dirName); + assertThat(urlPath).contains(file.getName()); + } +} \ No newline at end of file diff --git a/src/test/resources/test-image.png b/src/test/resources/test-image.png new file mode 100644 index 0000000000000000000000000000000000000000..1176d78ff76016df0b78897a90537305dd380d41 GIT binary patch literal 12119 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 s^+7nN> Date: Wed, 14 Aug 2024 05:50:17 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20=EB=B8=8C=EB=9E=9C=EB=93=9C=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=97=90=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../item/controller/AdminBrandController.java | 6 +++-- .../api/item/dto/BrandRequest.java | 4 +++- .../api/item/service/BrandService.java | 24 ++++++++++++------- .../ward_server/global/config/S3Config.java | 4 ---- .../global/config/S3MockConfig.java | 6 ++--- .../global/util/S3ImageUploader.java | 9 ++++--- src/main/resources/application.yml | 16 +++++++++++-- .../global/util/S3ImageUploaderTest.java | 4 +--- 8 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/ward/ward_server/api/item/controller/AdminBrandController.java b/src/main/java/com/ward/ward_server/api/item/controller/AdminBrandController.java index a3487f2..3150acd 100644 --- a/src/main/java/com/ward/ward_server/api/item/controller/AdminBrandController.java +++ b/src/main/java/com/ward/ward_server/api/item/controller/AdminBrandController.java @@ -7,6 +7,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.io.IOException; + import static com.ward.ward_server.global.response.ApiResponseMessage.*; @RestController @@ -16,12 +18,12 @@ public class AdminBrandController { private final BrandService brandService; @PostMapping - public ApiResponse createBrand(@RequestBody BrandRequest request) { + public ApiResponse createBrand(@ModelAttribute BrandRequest request) throws IOException { return ApiResponse.ok(BRAND_CREATE_SUCCESS, brandService.createBrand(request.koreanName(), request.englishName(), request.logoImage())); } @PatchMapping("/{brandId}") - public ApiResponse updateBrand(@PathVariable("brandId") long brandId, @RequestBody BrandRequest request) { + public ApiResponse updateBrand(@PathVariable("brandId") long brandId, @ModelAttribute BrandRequest request) throws IOException { return ApiResponse.ok(BRAND_UPDATE_SUCCESS, brandService.updateBrand(brandId, request.koreanName(), request.englishName(), request.logoImage())); } diff --git a/src/main/java/com/ward/ward_server/api/item/dto/BrandRequest.java b/src/main/java/com/ward/ward_server/api/item/dto/BrandRequest.java index 4802c83..a27917f 100644 --- a/src/main/java/com/ward/ward_server/api/item/dto/BrandRequest.java +++ b/src/main/java/com/ward/ward_server/api/item/dto/BrandRequest.java @@ -1,8 +1,10 @@ package com.ward.ward_server.api.item.dto; +import org.springframework.web.multipart.MultipartFile; + public record BrandRequest( String koreanName, String englishName, - String logoImage + MultipartFile logoImage ) { } diff --git a/src/main/java/com/ward/ward_server/api/item/service/BrandService.java b/src/main/java/com/ward/ward_server/api/item/service/BrandService.java index db98391..22c5c22 100644 --- a/src/main/java/com/ward/ward_server/api/item/service/BrandService.java +++ b/src/main/java/com/ward/ward_server/api/item/service/BrandService.java @@ -13,6 +13,7 @@ import com.ward.ward_server.global.Object.enums.BasicSort; import com.ward.ward_server.global.exception.ApiException; import com.ward.ward_server.global.exception.ExceptionCode; +import com.ward.ward_server.global.util.S3ImageUploader; import com.ward.ward_server.global.util.ValidationUtils; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -20,7 +21,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; import java.util.stream.Collectors; @@ -34,18 +37,20 @@ public class BrandService { private final BrandRepository brandRepository; private final ItemRepository itemRepository; private final ReleaseInfoRepository releaseInfoRepository; - + private final S3ImageUploader imageUploader; + private final String DIR_NAME="brand/logo"; @Transactional - public BrandResponse createBrand(String koreanName, String englishName, String brandLogoImage) { + public BrandResponse createBrand(String koreanName, String englishName, MultipartFile brandLogoImage) throws IOException { if (!StringUtils.hasText(koreanName) && !StringUtils.hasText(englishName)) { throw new ApiException(INVALID_INPUT, NAME_MUST_BE_PROVIDED.getMessage()); } ValidationUtils.validationNames(koreanName, englishName); - if (brandRepository.existsByKoreanNameOrEnglishName(koreanName, englishName)) + if (brandRepository.existsByKoreanNameOrEnglishName(koreanName, englishName)) { throw new ApiException(DUPLICATE_BRAND); - + } + String uploadedImageUrl=imageUploader.upload(brandLogoImage, DIR_NAME); Brand savedBrand = brandRepository.save(Brand.builder() - .logoImage(brandLogoImage) + .logoImage(uploadedImageUrl) .koreanName(koreanName) .englishName(englishName) .build()); @@ -79,13 +84,15 @@ public PageResponse getBrandReleaseInfoPage(long bran } @Transactional - public BrandResponse updateBrand(long brandId, String koreanName, String englishName, String brandLogoImage) { + public BrandResponse updateBrand(long brandId, String koreanName, String englishName, MultipartFile brandLogoImage) throws IOException { if (koreanName == null && englishName == null && brandLogoImage == null) throw new ApiException(ExceptionCode.INVALID_INPUT); ValidationUtils.validationNames(koreanName, englishName); Brand origin = brandRepository.findById(brandId).orElseThrow(() -> new ApiException(BRAND_NOT_FOUND)); - if (StringUtils.hasText(brandLogoImage)) { - origin.updateLogoImage(brandLogoImage); + if (brandLogoImage!=null) { + //TODO 기존 로고이미지 s3에서 삭제 로직 추가 + String uploadedImageUrl=imageUploader.upload(brandLogoImage, DIR_NAME); + origin.updateLogoImage(uploadedImageUrl); } if (StringUtils.hasText(koreanName)) { origin.updateKoreanName(koreanName); @@ -98,6 +105,7 @@ public BrandResponse updateBrand(long brandId, String koreanName, String english @Transactional public void deleteBrand(long brandId) { + //TODO 삭제하는 브랜드 로고이미지 s3에서 삭제 로직 추가 if (!brandRepository.existsById(brandId)) { throw new ApiException(BRAND_NOT_FOUND); } diff --git a/src/main/java/com/ward/ward_server/global/config/S3Config.java b/src/main/java/com/ward/ward_server/global/config/S3Config.java index 4cc6940..58d6056 100644 --- a/src/main/java/com/ward/ward_server/global/config/S3Config.java +++ b/src/main/java/com/ward/ward_server/global/config/S3Config.java @@ -4,13 +4,11 @@ import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration -@Slf4j public class S3Config { @Value("${cloud.aws.credentials.accessKey}") private String accessKey; @@ -23,8 +21,6 @@ public class S3Config { @Bean public AmazonS3Client amazonS3() { - //fixme debug로 고치기 - log.info("하늘:" + accessKey); BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); return (AmazonS3Client) AmazonS3ClientBuilder.standard() .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) diff --git a/src/main/java/com/ward/ward_server/global/config/S3MockConfig.java b/src/main/java/com/ward/ward_server/global/config/S3MockConfig.java index 79599ec..ddba700 100644 --- a/src/main/java/com/ward/ward_server/global/config/S3MockConfig.java +++ b/src/main/java/com/ward/ward_server/global/config/S3MockConfig.java @@ -22,18 +22,18 @@ public class S3MockConfig { @Bean(name = "s3Mock") public S3Mock s3Mock() { - log.info("s3 mock 빈 생성"); //fixme debug로 고치기 + log.debug("s3 mock 빈 생성"); S3Mock s3Mock = S3Mock.create(0, "s3mock"); Http.ServerBinding binding = s3Mock.start(); port = binding.localAddress().getPort(); - log.info("port:{}", port); + log.debug("port:{}", port); return s3Mock; } @Bean @DependsOn("s3Mock") public AmazonS3Client amazonS3() { - log.info("amazon client 빈 생성"); + log.debug("amazon client 빈 생성"); AwsClientBuilder.EndpointConfiguration endpoint = new AwsClientBuilder.EndpointConfiguration( "http://127.0.0.1:" + port, Regions.AP_NORTHEAST_2.name()); diff --git a/src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java b/src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java index a64af9c..a533cdf 100644 --- a/src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java +++ b/src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java @@ -31,7 +31,7 @@ public class S3ImageUploader { public String upload(MultipartFile multipartFile, String dirName) throws IOException { File uploadFile = convert(multipartFile) .orElseThrow(() -> new ApiException(INVALID_INPUT, FILE_CONVERT_FAIL.getMessage())); - log.info("uploadFile name: {}", uploadFile.getName()); //fixme debuf로 고치기 + log.debug("uploadFile name: {}", uploadFile.getName()); String fileName = dirName + "/" + uploadFile.getName(); String uploadImageUrl = putS3(uploadFile, fileName); uploadFile.delete(); @@ -52,14 +52,13 @@ public String getUrl(String bucket, String fileName) { private Optional convert(MultipartFile file) throws IOException { File convertFile = new File(file.getOriginalFilename()); - log.info("convertFile name: {}", convertFile.getName()); + log.debug("convertFile name: {}", convertFile.getName()); if (convertFile.createNewFile()) { - log.info("create new file"); + log.debug("create new file"); try (FileOutputStream fos = new FileOutputStream(convertFile)) { fos.write(file.getBytes()); } - return Optional.of(convertFile); } - return Optional.empty(); + return Optional.of(convertFile); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 85b2ab7..96a2f47 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,7 +2,7 @@ spring: jwt: secretKey: ${JWT_SECRET_KEY} password: ${JWT_PASSWORD} - accessTokenValidity: 15 # 분 단위 + accessTokenValidity: 1500 # 분 단위 refreshTokenValidity: 30 # 일 단위 datasource: @@ -56,4 +56,16 @@ logging: SQL: DEBUG type: descriptor: - sql: TRACE \ No newline at end of file + sql: TRACE + +cloud: + aws: + s3: + bucket: ${S3_BUCKET_NAME} + stack.auto: false + region.static: ${S3_BUCKET_REGION} + credentials: + accessKey: ${S3_ACCESS_KEY} + secretKey: ${S3_SECRET_KEY} + cloudFront: + domain: https://d95395pkgpbo8.cloudfront.net \ No newline at end of file diff --git a/src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java b/src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java index 6f90ec7..b5f7496 100644 --- a/src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java +++ b/src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java @@ -21,15 +21,13 @@ import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; + @Slf4j @ActiveProfiles("test") @ExtendWith(SpringExtension.class) @Import(S3MockConfig.class) @ContextConfiguration(classes = S3ImageUploader.class) class S3ImageUploaderTest { - - @Autowired private S3Mock s3Mock; @Autowired From 42ad0d67839302d4e9f5468e44d76f843bade6bb Mon Sep 17 00:00:00 2001 From: ubeuu Date: Fri, 16 Aug 2024 17:25:21 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EB=B8=8C=EB=9E=9C=EB=93=9C?= =?UTF-8?q?=EC=99=80=20=EC=83=81=ED=92=88=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=97=90=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../item/controller/AdminBrandController.java | 18 ++++- .../item/controller/AdminItemController.java | 27 +++++-- .../api/item/dto/BrandRequest.java | 6 +- .../ward_server/api/item/dto/ItemRequest.java | 4 - .../api/item/repository/BrandRepository.java | 3 + .../item/repository/ItemImageRepository.java | 7 -- .../api/item/repository/ItemRepository.java | 3 + .../api/item/service/BrandService.java | 32 ++++---- .../api/item/service/ItemService.java | 73 +++++++++++++------ ...ImageUploader.java => S3ImageManager.java} | 22 ++++-- ...oaderTest.java => S3ImageManagerTest.java} | 12 +-- 11 files changed, 133 insertions(+), 74 deletions(-) rename src/main/java/com/ward/ward_server/global/util/{S3ImageUploader.java => S3ImageManager.java} (75%) rename src/test/java/com/ward/ward_server/global/util/{S3ImageUploaderTest.java => S3ImageManagerTest.java} (83%) diff --git a/src/main/java/com/ward/ward_server/api/item/controller/AdminBrandController.java b/src/main/java/com/ward/ward_server/api/item/controller/AdminBrandController.java index 3150acd..06533df 100644 --- a/src/main/java/com/ward/ward_server/api/item/controller/AdminBrandController.java +++ b/src/main/java/com/ward/ward_server/api/item/controller/AdminBrandController.java @@ -5,12 +5,15 @@ import com.ward.ward_server.api.item.service.BrandService; import com.ward.ward_server.global.response.ApiResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import static com.ward.ward_server.global.response.ApiResponseMessage.*; +@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/admin/brands") @@ -18,13 +21,20 @@ public class AdminBrandController { private final BrandService brandService; @PostMapping - public ApiResponse createBrand(@ModelAttribute BrandRequest request) throws IOException { - return ApiResponse.ok(BRAND_CREATE_SUCCESS, brandService.createBrand(request.koreanName(), request.englishName(), request.logoImage())); + public ApiResponse createBrand(@RequestPart BrandRequest request, + @RequestPart(required = false) MultipartFile logoImage) throws IOException { + return ApiResponse.ok(BRAND_CREATE_SUCCESS, brandService.createBrand(request.koreanName(), request.englishName(), logoImage)); } @PatchMapping("/{brandId}") - public ApiResponse updateBrand(@PathVariable("brandId") long brandId, @ModelAttribute BrandRequest request) throws IOException { - return ApiResponse.ok(BRAND_UPDATE_SUCCESS, brandService.updateBrand(brandId, request.koreanName(), request.englishName(), request.logoImage())); + public ApiResponse updateBrand(@PathVariable("brandId") long brandId, + @RequestPart(required = false) BrandRequest request, + @RequestPart(required = false) MultipartFile logoImage) throws IOException { + if (request == null) { + return ApiResponse.ok(BRAND_UPDATE_SUCCESS, brandService.updateBrand(brandId, null, null, logoImage)); + } else { + return ApiResponse.ok(BRAND_UPDATE_SUCCESS, brandService.updateBrand(brandId, request.koreanName(), request.englishName(), logoImage)); + } } @DeleteMapping("/{brandId}") diff --git a/src/main/java/com/ward/ward_server/api/item/controller/AdminItemController.java b/src/main/java/com/ward/ward_server/api/item/controller/AdminItemController.java index 21b7649..eecf680 100644 --- a/src/main/java/com/ward/ward_server/api/item/controller/AdminItemController.java +++ b/src/main/java/com/ward/ward_server/api/item/controller/AdminItemController.java @@ -8,6 +8,10 @@ import com.ward.ward_server.global.response.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; import static com.ward.ward_server.global.response.ApiResponseMessage.*; @@ -27,16 +31,29 @@ public ApiResponse abc() { @PostMapping - public ApiResponse createItem(@RequestBody ItemRequest request) throws ApiException { + public ApiResponse createItem(@RequestPart ItemRequest request, + @RequestPart(required = false) MultipartFile mainImage, + @RequestPart(required = false) List itemImages) throws ApiException, IOException { return ApiResponse.ok(ITEM_CREATE_SUCCESS, - itemService.createItem(request.itemCode(), request.koreanName(), request.englishName(), request.mainImage(), request.itemImages(), request.brandId(), request.category(), request.price())); + itemService.createItem(request.itemCode(), request.koreanName(), request.englishName(), mainImage, itemImages, request.brandId(), request.category(), request.price())); } @PatchMapping("/{itemId}") public ApiResponse updateItem(@PathVariable("itemId") Long itemId, - @RequestBody ItemRequest request) { - return ApiResponse.ok(ITEM_UPDATE_SUCCESS, - itemService.updateItem(itemId, request.koreanName(), request.englishName(), request.itemCode(), request.mainImage(), request.itemImages(), request.brandId(), request.category(), request.price())); + @RequestPart(required = false) ItemRequest request, + @RequestPart(required = false) MultipartFile mainImage, + @RequestPart(required = false) List itemImages) throws IOException { + if (request == null) { + return ApiResponse.ok(ITEM_UPDATE_SUCCESS, + itemService.updateItem(itemId, + null, null, null, null, null, null, + mainImage, itemImages)); + } else { + return ApiResponse.ok(ITEM_UPDATE_SUCCESS, + itemService.updateItem(itemId, + request.koreanName(), request.englishName(), request.itemCode(), request.brandId(), request.category(), request.price(), + mainImage, itemImages)); + } } @DeleteMapping("/{itemId}") diff --git a/src/main/java/com/ward/ward_server/api/item/dto/BrandRequest.java b/src/main/java/com/ward/ward_server/api/item/dto/BrandRequest.java index a27917f..d312ce6 100644 --- a/src/main/java/com/ward/ward_server/api/item/dto/BrandRequest.java +++ b/src/main/java/com/ward/ward_server/api/item/dto/BrandRequest.java @@ -1,10 +1,6 @@ package com.ward.ward_server.api.item.dto; -import org.springframework.web.multipart.MultipartFile; - public record BrandRequest( String koreanName, - String englishName, - MultipartFile logoImage -) { + String englishName) { } diff --git a/src/main/java/com/ward/ward_server/api/item/dto/ItemRequest.java b/src/main/java/com/ward/ward_server/api/item/dto/ItemRequest.java index 2c8e800..fa42d6d 100644 --- a/src/main/java/com/ward/ward_server/api/item/dto/ItemRequest.java +++ b/src/main/java/com/ward/ward_server/api/item/dto/ItemRequest.java @@ -2,14 +2,10 @@ import com.ward.ward_server.api.item.entity.enums.Category; -import java.util.List; - public record ItemRequest( String koreanName, String englishName, String itemCode, - String mainImage, - List itemImages, Long brandId, Category category, Integer price) { diff --git a/src/main/java/com/ward/ward_server/api/item/repository/BrandRepository.java b/src/main/java/com/ward/ward_server/api/item/repository/BrandRepository.java index f7bbf94..c043f93 100644 --- a/src/main/java/com/ward/ward_server/api/item/repository/BrandRepository.java +++ b/src/main/java/com/ward/ward_server/api/item/repository/BrandRepository.java @@ -24,4 +24,7 @@ public interface BrandRepository extends JpaRepository, BrandQueryR "OR LOWER(b.englishName) LIKE LOWER(CONCAT('%', :keyword, '%')) " + "ORDER BY b.koreanName ASC") List searchBrands(@Param("keyword") String keyword); + + @Query("SELECT b.logoImage FROM Brand b where b.id = :brandId") + String findLogoImageByBrandId(long brandId); } diff --git a/src/main/java/com/ward/ward_server/api/item/repository/ItemImageRepository.java b/src/main/java/com/ward/ward_server/api/item/repository/ItemImageRepository.java index 99d97a0..837cbd3 100644 --- a/src/main/java/com/ward/ward_server/api/item/repository/ItemImageRepository.java +++ b/src/main/java/com/ward/ward_server/api/item/repository/ItemImageRepository.java @@ -1,17 +1,10 @@ package com.ward.ward_server.api.item.repository; -import com.ward.ward_server.api.item.entity.Item; import com.ward.ward_server.api.item.entity.ItemImage; import org.springframework.data.jpa.repository.JpaRepository; -import javax.swing.text.html.Option; import java.util.List; -import java.util.Optional; public interface ItemImageRepository extends JpaRepository { List findAllByItemId(long itemId); - - void deleteAllByItemId(long itemId); - - Optional findFirstByItemId(long itemId); } diff --git a/src/main/java/com/ward/ward_server/api/item/repository/ItemRepository.java b/src/main/java/com/ward/ward_server/api/item/repository/ItemRepository.java index 0d9f6af..14195ef 100644 --- a/src/main/java/com/ward/ward_server/api/item/repository/ItemRepository.java +++ b/src/main/java/com/ward/ward_server/api/item/repository/ItemRepository.java @@ -20,4 +20,7 @@ public interface ItemRepository extends JpaRepository, ItemQueryRepo "OR i.code LIKE %:keyword% " + "ORDER BY i.viewCount DESC") Page searchItems(@Param("keyword") String keyword, Pageable pageable); + + @Query("SELECT i.mainImage FROM Item i where i.id = :itemId") + String findMainImageByItemId(long itemId); } diff --git a/src/main/java/com/ward/ward_server/api/item/service/BrandService.java b/src/main/java/com/ward/ward_server/api/item/service/BrandService.java index 22c5c22..7d52660 100644 --- a/src/main/java/com/ward/ward_server/api/item/service/BrandService.java +++ b/src/main/java/com/ward/ward_server/api/item/service/BrandService.java @@ -12,8 +12,7 @@ import com.ward.ward_server.global.Object.PageResponse; import com.ward.ward_server.global.Object.enums.BasicSort; import com.ward.ward_server.global.exception.ApiException; -import com.ward.ward_server.global.exception.ExceptionCode; -import com.ward.ward_server.global.util.S3ImageUploader; +import com.ward.ward_server.global.util.S3ImageManager; import com.ward.ward_server.global.util.ValidationUtils; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -37,8 +36,9 @@ public class BrandService { private final BrandRepository brandRepository; private final ItemRepository itemRepository; private final ReleaseInfoRepository releaseInfoRepository; - private final S3ImageUploader imageUploader; - private final String DIR_NAME="brand/logo"; + private final S3ImageManager imageManager; + private final String DIR_NAME = "brand/logo"; + @Transactional public BrandResponse createBrand(String koreanName, String englishName, MultipartFile brandLogoImage) throws IOException { if (!StringUtils.hasText(koreanName) && !StringUtils.hasText(englishName)) { @@ -48,7 +48,7 @@ public BrandResponse createBrand(String koreanName, String englishName, Multipar if (brandRepository.existsByKoreanNameOrEnglishName(koreanName, englishName)) { throw new ApiException(DUPLICATE_BRAND); } - String uploadedImageUrl=imageUploader.upload(brandLogoImage, DIR_NAME); + String uploadedImageUrl = brandLogoImage != null ? imageManager.upload(brandLogoImage, DIR_NAME) : null; Brand savedBrand = brandRepository.save(Brand.builder() .logoImage(uploadedImageUrl) .koreanName(koreanName) @@ -85,14 +85,13 @@ public PageResponse getBrandReleaseInfoPage(long bran @Transactional public BrandResponse updateBrand(long brandId, String koreanName, String englishName, MultipartFile brandLogoImage) throws IOException { - if (koreanName == null && englishName == null && brandLogoImage == null) - throw new ApiException(ExceptionCode.INVALID_INPUT); ValidationUtils.validationNames(koreanName, englishName); Brand origin = brandRepository.findById(brandId).orElseThrow(() -> new ApiException(BRAND_NOT_FOUND)); - if (brandLogoImage!=null) { - //TODO 기존 로고이미지 s3에서 삭제 로직 추가 - String uploadedImageUrl=imageUploader.upload(brandLogoImage, DIR_NAME); - origin.updateLogoImage(uploadedImageUrl); + if (brandLogoImage != null) { + String originLogoImage = brandRepository.findLogoImageByBrandId(brandId); + imageManager.delete(originLogoImage); + String uploadedLogoImageUrl = imageManager.upload(brandLogoImage, DIR_NAME); + origin.updateLogoImage(uploadedLogoImageUrl); } if (StringUtils.hasText(koreanName)) { origin.updateKoreanName(koreanName); @@ -105,11 +104,9 @@ public BrandResponse updateBrand(long brandId, String koreanName, String english @Transactional public void deleteBrand(long brandId) { - //TODO 삭제하는 브랜드 로고이미지 s3에서 삭제 로직 추가 - if (!brandRepository.existsById(brandId)) { - throw new ApiException(BRAND_NOT_FOUND); - } - brandRepository.deleteById(brandId); + Brand brand = brandRepository.findById(brandId).orElseThrow(() -> new ApiException(BRAND_NOT_FOUND)); + imageManager.delete(brand.getLogoImage()); + brandRepository.delete(brand); } @Transactional @@ -122,7 +119,8 @@ public long increaseBrandViewCount(long brandId) { private BrandResponse getBrandResponse(Brand brand) { return new BrandResponse( brand.getId(), - brand.getLogoImage(), + brand.getLogoImage() == null ? + imageManager.getUrl(DIR_NAME + "/brand-basic-logo.png") : brand.getLogoImage(), brand.getKoreanName(), brand.getEnglishName(), brand.getViewCount() diff --git a/src/main/java/com/ward/ward_server/api/item/service/ItemService.java b/src/main/java/com/ward/ward_server/api/item/service/ItemService.java index bb4c661..7ee3f59 100644 --- a/src/main/java/com/ward/ward_server/api/item/service/ItemService.java +++ b/src/main/java/com/ward/ward_server/api/item/service/ItemService.java @@ -9,6 +9,7 @@ import com.ward.ward_server.global.Object.PageResponse; import com.ward.ward_server.global.Object.enums.Section; import com.ward.ward_server.global.exception.ApiException; +import com.ward.ward_server.global.util.S3ImageManager; import com.ward.ward_server.global.util.ValidationUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,7 +18,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -36,9 +39,12 @@ public class ItemService { private final ItemImageRepository itemImageRepository; private final ItemViewCountRepository itemViewCountRepository; private final ItemTopRankRepository itemTopRankRepository; + private final S3ImageManager imageManager; + private final String MAIN_IMAGE_DIR_NAME = "item/main"; + private final String SUB_IMAGES_DIR_NAME = "item/sub"; @Transactional - public ItemDetailResponse createItem(String itemCode, String koreanName, String englishName, String mainImage, List itemImages, Long brandId, Category category, Integer price) throws ApiException { + public ItemDetailResponse createItem(String itemCode, String koreanName, String englishName, MultipartFile mainImage, List itemImages, Long brandId, Category category, Integer price) throws ApiException, IOException { if (!StringUtils.hasText(koreanName) && !StringUtils.hasText(englishName)) { throw new ApiException(INVALID_INPUT, NAME_MUST_BE_PROVIDED.getMessage()); } @@ -46,21 +52,35 @@ public ItemDetailResponse createItem(String itemCode, String koreanName, String if (!StringUtils.hasText(itemCode) || brandId == null || category == null) { throw new ApiException(INVALID_INPUT, REQUIRED_FIELDS_MUST_BE_PROVIDED.getMessage()); } + Brand brand = brandRepository.findById(brandId).orElseThrow(() -> new ApiException(BRAND_NOT_FOUND)); - if (itemRepository.existsByCodeAndBrandId(itemCode, brand.getId())) throw new ApiException(DUPLICATE_ITEM); + if (itemRepository.existsByCodeAndBrandId(itemCode, brand.getId())) { + throw new ApiException(DUPLICATE_ITEM); + } + String uploadedMainImageUrl = mainImage != null ? imageManager.upload(mainImage, MAIN_IMAGE_DIR_NAME) : null; Item savedItem = itemRepository.save(Item.builder() .code(itemCode) .koreanName(koreanName) .englishName(englishName) - .mainImage(mainImage) + .mainImage(uploadedMainImageUrl) .brand(brand) .category(category) .price(price) .build()); - itemImages.stream() - .map(e -> ItemImage.builder().itemId(savedItem.getId()).url(e).build()) - .forEach(itemImageRepository::save); - + if (itemImages != null) { + itemImages.stream() + .map(e -> { + try { + return ItemImage.builder() + .itemId(savedItem.getId()) + .url(imageManager.upload(e, SUB_IMAGES_DIR_NAME)) + .build(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + }) + .forEach(itemImageRepository::save); + } // add 손지민: 실시간 Top10 을 위해 ItemViewCount 테이블 생성 ItemViewCount itemViewCount = ItemViewCount.builder() .category(savedItem.getCategory()) @@ -100,7 +120,6 @@ public List getItem10List(Long userId, Section section, Cate @Transactional(readOnly = true) public PageResponse getItemPage(Long userId, Section section, Category category, int page, String date) { - log.info("하늘:{}", date); return switch (section) { case RELEASE_SCHEDULE, CLOSED -> { Page itemPageInfo = itemRepository.getItemPage(userId, LocalDateTime.now().minusHours(9), category, section, date, PageRequest.of(page, API_PAGE_SIZE)); //HACK DB 시간 설정 전까지는 -9시간으로 비교해야 한다. @@ -135,10 +154,8 @@ private List convertToTopResponse(List topItem @Transactional public ItemDetailResponse updateItem(Long itemId, - String koreanName, String englishName, String itemCode, String mainImage, List itemImages, Long brandId, Category category, Integer price) { - if (koreanName == null && englishName == null && itemCode == null && itemImages == null && brandId == null && category == null && price == null) { - throw new ApiException(INVALID_INPUT); - } + String koreanName, String englishName, String itemCode, Long brandId, Category category, Integer price, + MultipartFile mainImage, List itemImages) throws IOException { Item origin = itemRepository.findById(itemId).orElseThrow(() -> new ApiException(ITEM_NOT_FOUND)); Brand brand = null; if (itemCode == null && brandId != null) { @@ -160,13 +177,25 @@ public ItemDetailResponse updateItem(Long itemId, origin.updateBrand(brand); origin.updateCode(itemCode); } - if (StringUtils.hasText(mainImage)) { - origin.updateMainImage(mainImage); + if (mainImage != null) { + String originMainImage = itemRepository.findMainImageByItemId(itemId); + imageManager.delete(originMainImage); + String uploadedImageUrl = imageManager.upload(mainImage, MAIN_IMAGE_DIR_NAME); + origin.updateMainImage(uploadedImageUrl); } if (itemImages != null && !itemImages.isEmpty()) { - itemImageRepository.deleteAllByItemId(origin.getId()); + //TODO 기존 이미지를 삭제하는 로직 혹은 api 추가(지민님과 의논) itemImages.stream() - .map(e -> ItemImage.builder().itemId(origin.getId()).url(e).build()) + .map(e -> { + try { + return ItemImage.builder() + .itemId(itemId) + .url(imageManager.upload(e, SUB_IMAGES_DIR_NAME)) + .build(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + }) .forEach(itemImageRepository::save); } if (category != null) { @@ -186,10 +215,11 @@ public ItemDetailResponse updateItem(Long itemId, @Transactional public void deleteItem(long itemId) { - if (!itemRepository.existsById(itemId)) { - throw new ApiException(ITEM_NOT_FOUND); - } - itemRepository.deleteById(itemId); + Item item = itemRepository.findById(itemId).orElseThrow(() -> new ApiException(ITEM_NOT_FOUND)); + imageManager.delete(item.getMainImage()); + itemImageRepository.findAllByItemId(itemId) + .forEach(i -> imageManager.delete(i.getUrl())); + itemRepository.delete(item); } private ItemDetailResponse getItemDetailResponse(Item item, Brand brand) { @@ -198,7 +228,8 @@ private ItemDetailResponse getItemDetailResponse(Item item, Brand brand) { item.getKoreanName(), item.getEnglishName(), item.getCode(), - item.getMainImage(), + item.getMainImage() == null ? + imageManager.getUrl(MAIN_IMAGE_DIR_NAME + "/item-basic-main-image.png") : item.getMainImage(), itemImageRepository.findAllByItemId(item.getId()).stream().map(ItemImage::getUrl).toList(), item.getViewCount(), item.getCategory().getDesc(), diff --git a/src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java b/src/main/java/com/ward/ward_server/global/util/S3ImageManager.java similarity index 75% rename from src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java rename to src/main/java/com/ward/ward_server/global/util/S3ImageManager.java index a533cdf..39f9f4a 100644 --- a/src/main/java/com/ward/ward_server/global/util/S3ImageUploader.java +++ b/src/main/java/com/ward/ward_server/global/util/S3ImageManager.java @@ -2,6 +2,7 @@ import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.amazonaws.services.s3.model.PutObjectRequest; import com.ward.ward_server.global.exception.ApiException; import lombok.RequiredArgsConstructor; @@ -21,7 +22,7 @@ @RequiredArgsConstructor @Component @Slf4j -public class S3ImageUploader { +public class S3ImageManager { private final AmazonS3Client amazonS3Client; @Value("${cloud.aws.s3.bucket}") private String bucket; @@ -32,8 +33,8 @@ public String upload(MultipartFile multipartFile, String dirName) throws IOExcep File uploadFile = convert(multipartFile) .orElseThrow(() -> new ApiException(INVALID_INPUT, FILE_CONVERT_FAIL.getMessage())); log.debug("uploadFile name: {}", uploadFile.getName()); - String fileName = dirName + "/" + uploadFile.getName(); - String uploadImageUrl = putS3(uploadFile, fileName); + String filePath = dirName + "/" + uploadFile.getName(); + String uploadImageUrl = putS3(uploadFile, filePath); uploadFile.delete(); return uploadImageUrl; } @@ -46,8 +47,8 @@ private String putS3(File uploadFile, String fileName) { return cloudFrontDomain + "/" + fileName; } - public String getUrl(String bucket, String fileName) { - return amazonS3Client.getUrl(bucket, fileName).toString(); + public String getUrl(String fileName) { + return cloudFrontDomain + "/" + fileName; } private Optional convert(MultipartFile file) throws IOException { @@ -61,4 +62,15 @@ private Optional convert(MultipartFile file) throws IOException { } return Optional.of(convertFile); } + + public void delete(String url) { + if (url == null) return; + String fileOriginName = extractFileOriginName(url); + log.debug("delete file origin name: {}", fileOriginName); + amazonS3Client.deleteObject(new DeleteObjectRequest(bucket, fileOriginName)); + } + + private String extractFileOriginName(String url) { + return url.substring(cloudFrontDomain.length() + 1); + } } diff --git a/src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java b/src/test/java/com/ward/ward_server/global/util/S3ImageManagerTest.java similarity index 83% rename from src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java rename to src/test/java/com/ward/ward_server/global/util/S3ImageManagerTest.java index b5f7496..96960b2 100644 --- a/src/test/java/com/ward/ward_server/global/util/S3ImageUploaderTest.java +++ b/src/test/java/com/ward/ward_server/global/util/S3ImageManagerTest.java @@ -26,12 +26,12 @@ @ActiveProfiles("test") @ExtendWith(SpringExtension.class) @Import(S3MockConfig.class) -@ContextConfiguration(classes = S3ImageUploader.class) -class S3ImageUploaderTest { +@ContextConfiguration(classes = S3ImageManager.class) +class S3ImageManagerTest { @Autowired private S3Mock s3Mock; @Autowired - private S3ImageUploader s3ImageUploader; + private S3ImageManager s3ImageManager; @AfterEach public void tearDown() { @@ -52,11 +52,11 @@ public void tearDown() { "image/png", new FileInputStream(file) ); - ReflectionTestUtils.setField(s3ImageUploader, "bucket", mockBucketName); - ReflectionTestUtils.setField(s3ImageUploader, "cloudFrontDomain", mockCloudFrontDomain); + ReflectionTestUtils.setField(s3ImageManager, "bucket", mockBucketName); + ReflectionTestUtils.setField(s3ImageManager, "cloudFrontDomain", mockCloudFrontDomain); // when - String urlPath = s3ImageUploader.upload(multipartFile, dirName); + String urlPath = s3ImageManager.upload(multipartFile, dirName); // then assertThat(urlPath).contains(mockCloudFrontDomain); From 3c5ff5d1a585ca712303a7241f3512004592f136 Mon Sep 17 00:00:00 2001 From: ubeuu Date: Sat, 17 Aug 2024 23:45:19 +0900 Subject: [PATCH 4/6] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20=EC=9D=BC?= =?UTF-8?q?=EB=B6=80=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../item/controller/AdminItemController.java | 3 +- .../api/item/repository/BrandRepository.java | 3 - .../api/item/repository/ItemRepository.java | 3 - .../api/item/service/BrandService.java | 3 +- .../api/item/service/ItemService.java | 9 +- .../api/item/service/BrandServiceTest.java | 168 +++++++++---- .../api/item/service/ItemServiceTest.java | 221 +++++++++++++++--- 7 files changed, 314 insertions(+), 96 deletions(-) diff --git a/src/main/java/com/ward/ward_server/api/item/controller/AdminItemController.java b/src/main/java/com/ward/ward_server/api/item/controller/AdminItemController.java index eecf680..5290e38 100644 --- a/src/main/java/com/ward/ward_server/api/item/controller/AdminItemController.java +++ b/src/main/java/com/ward/ward_server/api/item/controller/AdminItemController.java @@ -35,7 +35,8 @@ public ApiResponse createItem(@RequestPart ItemRequest reque @RequestPart(required = false) MultipartFile mainImage, @RequestPart(required = false) List itemImages) throws ApiException, IOException { return ApiResponse.ok(ITEM_CREATE_SUCCESS, - itemService.createItem(request.itemCode(), request.koreanName(), request.englishName(), mainImage, itemImages, request.brandId(), request.category(), request.price())); + itemService.createItem(request.itemCode(), request.koreanName(), request.englishName(), request.brandId(), request.category(), request.price(), + mainImage, itemImages)); } @PatchMapping("/{itemId}") diff --git a/src/main/java/com/ward/ward_server/api/item/repository/BrandRepository.java b/src/main/java/com/ward/ward_server/api/item/repository/BrandRepository.java index c043f93..f7bbf94 100644 --- a/src/main/java/com/ward/ward_server/api/item/repository/BrandRepository.java +++ b/src/main/java/com/ward/ward_server/api/item/repository/BrandRepository.java @@ -24,7 +24,4 @@ public interface BrandRepository extends JpaRepository, BrandQueryR "OR LOWER(b.englishName) LIKE LOWER(CONCAT('%', :keyword, '%')) " + "ORDER BY b.koreanName ASC") List searchBrands(@Param("keyword") String keyword); - - @Query("SELECT b.logoImage FROM Brand b where b.id = :brandId") - String findLogoImageByBrandId(long brandId); } diff --git a/src/main/java/com/ward/ward_server/api/item/repository/ItemRepository.java b/src/main/java/com/ward/ward_server/api/item/repository/ItemRepository.java index 14195ef..0d9f6af 100644 --- a/src/main/java/com/ward/ward_server/api/item/repository/ItemRepository.java +++ b/src/main/java/com/ward/ward_server/api/item/repository/ItemRepository.java @@ -20,7 +20,4 @@ public interface ItemRepository extends JpaRepository, ItemQueryRepo "OR i.code LIKE %:keyword% " + "ORDER BY i.viewCount DESC") Page searchItems(@Param("keyword") String keyword, Pageable pageable); - - @Query("SELECT i.mainImage FROM Item i where i.id = :itemId") - String findMainImageByItemId(long itemId); } diff --git a/src/main/java/com/ward/ward_server/api/item/service/BrandService.java b/src/main/java/com/ward/ward_server/api/item/service/BrandService.java index 7d52660..5ae73a7 100644 --- a/src/main/java/com/ward/ward_server/api/item/service/BrandService.java +++ b/src/main/java/com/ward/ward_server/api/item/service/BrandService.java @@ -88,8 +88,7 @@ public BrandResponse updateBrand(long brandId, String koreanName, String english ValidationUtils.validationNames(koreanName, englishName); Brand origin = brandRepository.findById(brandId).orElseThrow(() -> new ApiException(BRAND_NOT_FOUND)); if (brandLogoImage != null) { - String originLogoImage = brandRepository.findLogoImageByBrandId(brandId); - imageManager.delete(originLogoImage); + imageManager.delete(origin.getLogoImage()); String uploadedLogoImageUrl = imageManager.upload(brandLogoImage, DIR_NAME); origin.updateLogoImage(uploadedLogoImageUrl); } diff --git a/src/main/java/com/ward/ward_server/api/item/service/ItemService.java b/src/main/java/com/ward/ward_server/api/item/service/ItemService.java index 7ee3f59..4d9667f 100644 --- a/src/main/java/com/ward/ward_server/api/item/service/ItemService.java +++ b/src/main/java/com/ward/ward_server/api/item/service/ItemService.java @@ -44,7 +44,8 @@ public class ItemService { private final String SUB_IMAGES_DIR_NAME = "item/sub"; @Transactional - public ItemDetailResponse createItem(String itemCode, String koreanName, String englishName, MultipartFile mainImage, List itemImages, Long brandId, Category category, Integer price) throws ApiException, IOException { + public ItemDetailResponse createItem(String itemCode, String koreanName, String englishName, Long brandId, Category category, Integer price, + MultipartFile mainImage, List itemImages) throws ApiException, IOException { if (!StringUtils.hasText(koreanName) && !StringUtils.hasText(englishName)) { throw new ApiException(INVALID_INPUT, NAME_MUST_BE_PROVIDED.getMessage()); } @@ -109,7 +110,6 @@ public void increaseViewCount(Item item) { .build()); } - @Transactional(readOnly = true) public List getItem10List(Long userId, Section section, Category category) { return switch (section) { case DUE_TODAY, RELEASE_NOW, RELEASE_SCHEDULE -> @@ -118,7 +118,6 @@ public List getItem10List(Long userId, Section section, Cate }; } - @Transactional(readOnly = true) public PageResponse getItemPage(Long userId, Section section, Category category, int page, String date) { return switch (section) { case RELEASE_SCHEDULE, CLOSED -> { @@ -129,7 +128,6 @@ public PageResponse getItemPage(Long userId, Section section }; } - @Transactional(readOnly = true) public List getTopItemsResponseByCategory(Category category, int limit) { List topItems; if (category == Category.ALL) { @@ -178,8 +176,7 @@ public ItemDetailResponse updateItem(Long itemId, origin.updateCode(itemCode); } if (mainImage != null) { - String originMainImage = itemRepository.findMainImageByItemId(itemId); - imageManager.delete(originMainImage); + imageManager.delete(origin.getMainImage()); String uploadedImageUrl = imageManager.upload(mainImage, MAIN_IMAGE_DIR_NAME); origin.updateMainImage(uploadedImageUrl); } diff --git a/src/test/java/com/ward/ward_server/api/item/service/BrandServiceTest.java b/src/test/java/com/ward/ward_server/api/item/service/BrandServiceTest.java index b6011cf..a53447a 100644 --- a/src/test/java/com/ward/ward_server/api/item/service/BrandServiceTest.java +++ b/src/test/java/com/ward/ward_server/api/item/service/BrandServiceTest.java @@ -1,44 +1,124 @@ -//package com.ward.ward_server.api.item.service; -// -//import com.ward.ward_server.api.item.dto.BrandRecommendedResponse; -//import com.ward.ward_server.api.item.entity.Brand; -//import com.ward.ward_server.api.item.repository.BrandRepository; -//import org.junit.jupiter.api.Test; -//import org.mockito.InjectMocks; -//import org.mockito.Mock; -//import org.mockito.junit.jupiter.MockitoExtension; -//import org.junit.jupiter.api.extension.ExtendWith; -// -//import java.util.List; -// -//import static org.junit.jupiter.api.Assertions.assertEquals; -//import static org.mockito.Mockito.when; -// -//@ExtendWith(MockitoExtension.class) -//public class BrandServiceTest { -// -// @Mock -// private BrandRepository brandRepository; -// @InjectMocks -// private BrandService brandService; -// -// @Test -// void testGetRecommendedBrands() { -// List mockBrands = List.of( -// Brand.builder().logoImage("https://example.com/logo1.png").koreanName("브랜드1").englishName("Brand1").build(), -// Brand.builder().logoImage("https://example.com/logo2.png").koreanName("브랜드2").englishName("Brand2").build() -// ); -// -// when(brandRepository.findTop10ByOrderByViewCountDesc()).thenReturn(mockBrands); -// -// List result = brandService.getRecommendedBrands(); -// -// assertEquals(2, result.size()); -// assertEquals("브랜드1", result.get(0).koreanName()); -// assertEquals("https://example.com/logo1.png", result.get(0).logoImage()); -// assertEquals("Brand1", result.get(0).englishName()); -// assertEquals("브랜드2", result.get(1).koreanName()); -// assertEquals("https://example.com/logo2.png", result.get(1).logoImage()); -// assertEquals("Brand2", result.get(1).englishName()); -// } -//} +package com.ward.ward_server.api.item.service; + +import com.ward.ward_server.api.item.dto.BrandRecommendedResponse; +import com.ward.ward_server.api.item.dto.BrandResponse; +import com.ward.ward_server.api.item.entity.Brand; +import com.ward.ward_server.api.item.repository.BrandRepository; +import com.ward.ward_server.api.item.repository.ItemRepository; +import com.ward.ward_server.api.releaseInfo.repository.ReleaseInfoRepository; +import com.ward.ward_server.global.util.S3ImageManager; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class BrandServiceTest { + @Mock + private BrandRepository brandRepository; + @Mock + private ItemRepository itemRepository; + @Mock + private ReleaseInfoRepository releaseInfoRepository; + @Mock + private S3ImageManager s3ImageManager; + @InjectMocks + private BrandService brandService; + + @Test + void testGetRecommendedBrands() { + List mockBrands = List.of( + Brand.builder().logoImage("https://example.com/logo1.png").koreanName("브랜드1").englishName("Brand1").build(), + Brand.builder().logoImage("https://example.com/logo2.png").koreanName("브랜드2").englishName("Brand2").build() + ); + + when(brandRepository.findTop10ByOrderByViewCountDesc()).thenReturn(mockBrands); + + List result = brandService.getRecommendedBrands(); + + assertEquals(2, result.size()); + assertEquals("브랜드1", result.get(0).koreanName()); + assertEquals("https://example.com/logo1.png", result.get(0).logoImage()); + assertEquals("Brand1", result.get(0).englishName()); + assertEquals("브랜드2", result.get(1).koreanName()); + assertEquals("https://example.com/logo2.png", result.get(1).logoImage()); + assertEquals("Brand2", result.get(1).englishName()); + } + + @Test + void 브랜드_생성시_로고_이미지를_입력하지_않으면_기본_이미지로_출력한다() throws IOException { + //given + long id = 1L; + String koreanName = "전 브랜드한글이름"; + String englishName = "before brand englishName"; + Brand mockBrand = Brand.builder() + .koreanName(koreanName) + .englishName(englishName) + .logoImage(null) + .build(); + ReflectionTestUtils.setField(mockBrand, "id", id); + when(brandRepository.save(any())).thenReturn(mockBrand); + + when(brandRepository.existsByKoreanNameOrEnglishName(koreanName, englishName)).thenReturn(false); + String mockBasicLogoImageUrl = "https://mock.cloudfront.net"; + when(s3ImageManager.getUrl(Mockito.anyString())).thenReturn(mockBasicLogoImageUrl); + //when + BrandResponse result = brandService.createBrand(koreanName, englishName, null); + //then + assertThat(result.id()).isEqualTo(id); + assertThat(result.koreanName()).isEqualTo(koreanName); + assertThat(result.englishName()).isEqualTo(englishName); + assertThat(result.logoImage()).isEqualTo(mockBasicLogoImageUrl); + } + + @Test + void 브랜드_수정_로직을_확인한다() throws IOException { + //given + long originBrandId = 1L; + String originBrandKoreanName = "전 브랜드한글이름"; + String originBrandEnglishName = "before brand englishName"; + String originBrandLogoImage = "https://mock-before-brand-logo-image.net"; + Brand mockBrand = Brand.builder() + .koreanName(originBrandKoreanName) + .englishName(originBrandEnglishName) + .logoImage(originBrandLogoImage) + .build(); + ReflectionTestUtils.setField(mockBrand, "id", originBrandId); + when(brandRepository.findById(originBrandId)).thenReturn(Optional.of(mockBrand)); + + String targetBrandKoreanName = "수정후 브랜드한글이름"; + String targetBrandEnglishName = "after brand englishName"; + MultipartFile targetMultipartFile = new MockMultipartFile( + "test", + "mock-logo.png", + "image/png", + "test-logo".getBytes() + ); + + String mockBrandLogoImageUrl = "https://mock.cloudfront.net"; + when(s3ImageManager.upload(eq(targetMultipartFile), anyString())).thenReturn(mockBrandLogoImageUrl); + + //when + BrandResponse result = brandService.updateBrand(originBrandId, targetBrandKoreanName, targetBrandEnglishName, targetMultipartFile); + + //then + assertThat(result.id()).isEqualTo(originBrandId); + assertThat(result.koreanName()).isEqualTo(targetBrandKoreanName); + assertThat(result.englishName()).isEqualTo(targetBrandEnglishName); + assertThat(result.logoImage()).isEqualTo(mockBrandLogoImageUrl); + } +} diff --git a/src/test/java/com/ward/ward_server/api/item/service/ItemServiceTest.java b/src/test/java/com/ward/ward_server/api/item/service/ItemServiceTest.java index 1b79a85..78b5611 100644 --- a/src/test/java/com/ward/ward_server/api/item/service/ItemServiceTest.java +++ b/src/test/java/com/ward/ward_server/api/item/service/ItemServiceTest.java @@ -1,37 +1,184 @@ -//package com.ward.ward_server.api.item.service; -// -//import com.ward.ward_server.api.item.entity.enums.Category; -//import com.ward.ward_server.api.item.repository.ItemRepository; -//import com.ward.ward_server.global.Object.enums.Section; -//import com.ward.ward_server.global.exception.ApiException; -//import org.junit.jupiter.api.Test; -//import org.junit.jupiter.api.extension.ExtendWith; -//import org.mockito.InjectMocks; -//import org.mockito.Mock; -//import org.mockito.Mockito; -//import org.mockito.junit.jupiter.MockitoExtension; -// -//import static com.ward.ward_server.global.exception.ExceptionCode.INVALID_INPUT; -//import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -// -//@ExtendWith(MockitoExtension.class) -//class ItemServiceTest { -// @Mock -// ItemRepository itemRepository; -// @InjectMocks -// ItemService itemService; -// -// @Test -// void 제공하지_않는_섹션으로_접근시_예외를_발생한다_10List() { -// assertThatExceptionOfType(ApiException.class) -// .isThrownBy(() -> itemService.getItem10List(1L, Section.CLOSED, Category.FOOTWEAR)) -// .withMessage(INVALID_INPUT.getMessage()); -// } -// -// @Test -// void 제공하지_않는_섹션으로_접근시_예외를_발생한다_page() { -// assertThatExceptionOfType(ApiException.class) -// .isThrownBy(() -> itemService.getItemPage(1L, Section.REGISTER_TODAY, Category.FOOTWEAR, 1, "2024-07")) -// .withMessage(INVALID_INPUT.getMessage()); -// } -//} \ No newline at end of file +package com.ward.ward_server.api.item.service; + +import com.ward.ward_server.api.item.dto.ItemDetailResponse; +import com.ward.ward_server.api.item.entity.Brand; +import com.ward.ward_server.api.item.entity.Item; +import com.ward.ward_server.api.item.entity.enums.Category; +import com.ward.ward_server.api.item.repository.*; +import com.ward.ward_server.global.Object.enums.Section; +import com.ward.ward_server.global.exception.ApiException; +import com.ward.ward_server.global.util.S3ImageManager; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Optional; + +import static com.ward.ward_server.global.exception.ExceptionCode.INVALID_INPUT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ItemServiceTest { + @Mock + ItemRepository itemRepository; + @Mock + BrandRepository brandRepository; + @Mock + ItemImageRepository itemImageRepository; + @Mock + ItemViewCountRepository itemViewCountRepository; + @Mock + ItemTopRankRepository itemTopRankRepository; + @Mock + S3ImageManager s3ImageManager; + @InjectMocks + ItemService itemService; + + @Test + void 제공하지_않는_섹션으로_접근시_예외를_발생한다_10List() { + assertThatExceptionOfType(ApiException.class) + .isThrownBy(() -> itemService.getItem10List(1L, Section.CLOSED, Category.FOOTWEAR)) + .withMessage(INVALID_INPUT.getMessage()); + } + + @Test + void 제공하지_않는_섹션으로_접근시_예외를_발생한다_page() { + assertThatExceptionOfType(ApiException.class) + .isThrownBy(() -> itemService.getItemPage(1L, Section.REGISTER_TODAY, Category.FOOTWEAR, 1, "2024-07")) + .withMessage(INVALID_INPUT.getMessage()); + } + + @Test + void 상품_생성시_메인_이미지를_입력하지_않으면_기본_이미지로_출력한다() throws IOException { + //given + long itemId = 1L; + String itemCode = "상품코드"; + String itemKoreanName = "상품한글이름"; + String itemEnglishName = "itemEnglishName"; + Category category = Category.FOOTWEAR; + int price = 10000; + Item mockItem = Item.builder() + .code(itemCode) + .koreanName(itemKoreanName) + .englishName(itemEnglishName) + .category(category) + .price(price) + .build(); + ReflectionTestUtils.setField(mockItem, "id", itemId); + when(itemRepository.save(Mockito.any())).thenReturn(mockItem); + + long brandId = 1L; + String brandKoreanName = "브랜드한글이름"; + String brandEnglishName = "brandEnglishName"; + String brandLogoImage = "https://mock-brand-logo-image.net"; + Brand mockBrand = Brand.builder() + .koreanName(brandKoreanName) + .englishName(brandEnglishName) + .logoImage(brandLogoImage) + .build(); + ReflectionTestUtils.setField(mockBrand, "id", brandId); + when(brandRepository.findById(brandId)).thenReturn(Optional.of(mockBrand)); + + String mockBasicMainImageUrl = "https://mock.cloudfront.net"; + when(s3ImageManager.getUrl(Mockito.anyString())).thenReturn(mockBasicMainImageUrl); + + when(itemRepository.existsByCodeAndBrandId(itemCode, brandId)).thenReturn(false); + + //when + ItemDetailResponse result = itemService.createItem(itemCode, itemKoreanName, itemEnglishName, brandId, category, price, null, null); + + //then + assertThat(result.itemId()).isEqualTo(itemId); + assertThat(result.itemKoreanName()).isEqualTo(itemKoreanName); + assertThat(result.itemEnglishName()).isEqualTo(itemEnglishName); + assertThat(result.itemCode()).isEqualTo(itemCode); + assertThat(result.mainImage()).isEqualTo(mockBasicMainImageUrl); + assertThat(result.itemImages()).isEqualTo(new ArrayList<>()); + assertThat(result.viewCount()).isEqualTo(0); + assertThat(result.category()).isEqualTo(category.getDesc()); + assertThat(result.price()).isEqualTo(price); + + assertThat(result.brandId()).isEqualTo(brandId); + assertThat(result.brandKoreanName()).isEqualTo(brandKoreanName); + assertThat(result.brandEnglishName()).isEqualTo(brandEnglishName); + assertThat(result.brandLogoImage()).isEqualTo(brandLogoImage); + } + + @Test + void 이미지를_제외한_상품_수정_로직을_확인한다() throws IOException { + //given + long itemId = 1L; + String originItemCode = "전 상품코드"; + String originItemKoreanName = "전 상품한글이름"; + String originItemEnglishName = "before item englishName"; + Category originCategory = Category.FOOTWEAR; + int originPrice = 10000; + Item mockItem = Item.builder() + .code(originItemCode) + .koreanName(originItemKoreanName) + .englishName(originItemEnglishName) + .category(originCategory) + .price(originPrice) + .build(); + ReflectionTestUtils.setField(mockItem, "id", itemId); + when(itemRepository.findById(itemId)).thenReturn(Optional.of(mockItem)); + + long targetBrandId = 2L; + String targetBrandKoreanName = "수정후 브랜드한글이름"; + String targetBrandEnglishName = "after brand englishName"; + String targetBrandLogoImage = "https://mock-after-brand-logo-image.net"; + Brand mockTargetBrand = Brand.builder() + .koreanName(targetBrandKoreanName) + .englishName(targetBrandEnglishName) + .logoImage(targetBrandLogoImage) + .build(); + ReflectionTestUtils.setField(mockTargetBrand, "id", targetBrandId); + when(brandRepository.findById(targetBrandId)).thenReturn(Optional.of(mockTargetBrand)); + + when(itemRepository.existsByCodeAndBrandId(Mockito.anyString(), Mockito.anyLong())).thenReturn(false); + + String mockBasicMainImageUrl = "https://mock.cloudfront.net"; + when(s3ImageManager.getUrl(Mockito.anyString())).thenReturn(mockBasicMainImageUrl); + + String targetItemKoreanName = "수정후 상품한글이름"; + String targetItemEnglishName = "after item englishName"; + String targetItemCode = "수정후 상품코드"; + Category targetCategory = Category.ACCESSORY; + int targetPrice = 20000; + + //when + ItemDetailResponse result = itemService.updateItem(itemId, + targetItemKoreanName, targetItemEnglishName, targetItemCode, targetBrandId, targetCategory, targetPrice, + null, null); + + //then + assertThat(result.itemId()).isEqualTo(itemId); + assertThat(result.itemKoreanName()).isEqualTo(targetItemKoreanName); + assertThat(result.itemEnglishName()).isEqualTo(targetItemEnglishName); + assertThat(result.itemCode()).isEqualTo(targetItemCode); + assertThat(result.mainImage()).isEqualTo(mockBasicMainImageUrl); + assertThat(result.itemImages()).isEqualTo(new ArrayList<>()); + assertThat(result.viewCount()).isEqualTo(0); + assertThat(result.category()).isEqualTo(targetCategory.getDesc()); + assertThat(result.price()).isEqualTo(targetPrice); + + assertThat(result.brandId()).isEqualTo(targetBrandId); + assertThat(result.brandKoreanName()).isEqualTo(targetBrandKoreanName); + assertThat(result.brandEnglishName()).isEqualTo(targetBrandEnglishName); + assertThat(result.brandLogoImage()).isEqualTo(targetBrandLogoImage); + } + + @Test + void 이미지_수정_로직을_확인한다(){ + //TODO + } +} \ No newline at end of file From c051b7eee307dccfb52c0aace5a6f0395e17ba3e Mon Sep 17 00:00:00 2001 From: ubeuu Date: Sun, 18 Aug 2024 00:30:35 +0900 Subject: [PATCH 5/6] =?UTF-8?q?test:=20=EB=8B=A8=EC=9C=84=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A7=8C=20=EC=8B=A4=ED=96=89=EC=A4=91?= =?UTF-8?q?=EC=9D=B4=EB=AF=80=EB=A1=9C=20=EC=9E=A0=EC=8B=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ward/ward_server/WardServerApplicationTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/ward/ward_server/WardServerApplicationTests.java b/src/test/java/com/ward/ward_server/WardServerApplicationTests.java index 9dc2e25..840e2da 100644 --- a/src/test/java/com/ward/ward_server/WardServerApplicationTests.java +++ b/src/test/java/com/ward/ward_server/WardServerApplicationTests.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest class WardServerApplicationTests { @Test From 9cbaa869b4d5f944c378018a237de847ae3fba63 Mon Sep 17 00:00:00 2001 From: ubeuu Date: Sun, 18 Aug 2024 21:44:20 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ward/ward_server/global/util/S3ImageManagerTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/ward/ward_server/global/util/S3ImageManagerTest.java b/src/test/java/com/ward/ward_server/global/util/S3ImageManagerTest.java index 96960b2..3d03fae 100644 --- a/src/test/java/com/ward/ward_server/global/util/S3ImageManagerTest.java +++ b/src/test/java/com/ward/ward_server/global/util/S3ImageManagerTest.java @@ -25,8 +25,10 @@ @Slf4j @ActiveProfiles("test") @ExtendWith(SpringExtension.class) -@Import(S3MockConfig.class) -@ContextConfiguration(classes = S3ImageManager.class) +@ContextConfiguration(classes = { + S3ImageManager.class, + S3MockConfig.class +}) class S3ImageManagerTest { @Autowired private S3Mock s3Mock;