From 178f990ed1268e66bfbf7d81b9b307166711a309 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Sat, 19 Oct 2024 23:06:01 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat=20:=20s3=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../com/get_offer/common/s3/S3Config.kt | 28 +++++++++ .../get_offer/common/s3/S3FileManagement.kt | 63 +++++++++++++++++++ .../com/get_offer/multipart/FileValidate.kt | 20 ++++++ .../com/get_offer/multipart/ImageService.kt | 18 ++++++ .../product/controller/ProductController.kt | 18 +++++- src/main/resources/application.yml | 10 ++- 7 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/get_offer/common/s3/S3Config.kt create mode 100644 src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt create mode 100644 src/main/kotlin/com/get_offer/multipart/FileValidate.kt create mode 100644 src/main/kotlin/com/get_offer/multipart/ImageService.kt diff --git a/build.gradle b/build.gradle index 9e195eb..855c650 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' implementation 'org.jetbrains.kotlin:kotlin-reflect' + implementation("com.amazonaws:aws-java-sdk-s3:1.12.773") compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/kotlin/com/get_offer/common/s3/S3Config.kt b/src/main/kotlin/com/get_offer/common/s3/S3Config.kt new file mode 100644 index 0000000..b28d6b0 --- /dev/null +++ b/src/main/kotlin/com/get_offer/common/s3/S3Config.kt @@ -0,0 +1,28 @@ +package com.get_offer.common.s3 + +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.regions.Regions +import com.amazonaws.services.s3.AmazonS3 +import com.amazonaws.services.s3.AmazonS3ClientBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class S3Config( + @Value("\${aws.credentials.accessKey}") + private val accessKey: String, + @Value("\${aws.credentials.secretKey}") + private val secretKey: String, +) { + @Bean + fun amazonS3Client(): AmazonS3 { + val credentials = BasicAWSCredentials(accessKey, secretKey) + return AmazonS3ClientBuilder + .standard() + .withCredentials(AWSStaticCredentialsProvider(credentials)) + .withRegion(Regions.AP_NORTHEAST_2) + .build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt b/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt new file mode 100644 index 0000000..b814fec --- /dev/null +++ b/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt @@ -0,0 +1,63 @@ +package com.get_offer.common.s3 + +import com.amazonaws.services.s3.AmazonS3 +import com.amazonaws.services.s3.model.ObjectMetadata +import com.get_offer.common.exception.NotFoundException +import com.get_offer.multipart.FileValidate +import java.util.* +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.multipart.MultipartFile + +@Component +class S3FileManagement( + @Value("\${aws.s3.bucket}") + private val bucket: String, + private val amazonS3: AmazonS3, +) { + companion object { + const val TYPE_IMAGE = "image" + } + + fun uploadImages(multipartFiles: List): List { + return multipartFiles.map { uploadImage(it) } + } + + fun uploadImage(multipartFile: MultipartFile): String { + val originalFilename = multipartFile.originalFilename + ?: throw NotFoundException("") + FileValidate.checkImageFormat(originalFilename) + val fileName = "${UUID.randomUUID()}-${originalFilename}" + val objectMetadata = setFileDateOption( + type = TYPE_IMAGE, + file = getFileExtension(originalFilename), + multipartFile = multipartFile + ) + amazonS3.putObject(bucket, fileName, multipartFile.inputStream, objectMetadata) + return fileName + } + + fun getFile(fileName: String): String { + return amazonS3.getUrl(bucket, fileName).toString() + } + + fun delete(fileName: String) { + amazonS3.deleteObject(bucket, fileName) + } + + private fun getFileExtension(fileName: String): String { + val extensionIndex = fileName.lastIndexOf('.') + return fileName.substring(extensionIndex + 1) + } + + private fun setFileDateOption( + type: String, + file: String, + multipartFile: MultipartFile + ): ObjectMetadata { + val objectMetadata = ObjectMetadata() + objectMetadata.contentType = "/${type}/${getFileExtension(file)}" + objectMetadata.contentLength = multipartFile.inputStream.available().toLong() + return objectMetadata + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/multipart/FileValidate.kt b/src/main/kotlin/com/get_offer/multipart/FileValidate.kt new file mode 100644 index 0000000..af2bbe0 --- /dev/null +++ b/src/main/kotlin/com/get_offer/multipart/FileValidate.kt @@ -0,0 +1,20 @@ +package com.get_offer.multipart + +import com.get_offer.common.exception.NotFoundException + +class FileValidate { + companion object { + private val IMAGE_EXTENSIONS: List = listOf("jpg", "png") + + fun checkImageFormat(fileName: String) { + val extensionIndex = fileName.lastIndexOf('.') + if (extensionIndex == -1) { + throw NotFoundException("") // Not exists file extension + } + val extension = fileName.substring(extensionIndex + 1) + require(IMAGE_EXTENSIONS.contains(extension)) { + throw NotFoundException("") // Not exists file extension + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/multipart/ImageService.kt b/src/main/kotlin/com/get_offer/multipart/ImageService.kt new file mode 100644 index 0000000..4b8da74 --- /dev/null +++ b/src/main/kotlin/com/get_offer/multipart/ImageService.kt @@ -0,0 +1,18 @@ +package com.get_offer.multipart + +import com.get_offer.common.s3.S3FileManagement +import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile + +@Service +class ImageService( + private val s3FileManagement: S3FileManagement, +) { + fun saveImages(images: List): List { + return s3FileManagement.uploadImages(images) + } + + fun deleteImage(imageUrl: String) { + val file = s3FileManagement.delete(imageUrl) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/controller/ProductController.kt b/src/main/kotlin/com/get_offer/product/controller/ProductController.kt index 3e1da6b..af83430 100644 --- a/src/main/kotlin/com/get_offer/product/controller/ProductController.kt +++ b/src/main/kotlin/com/get_offer/product/controller/ProductController.kt @@ -1,20 +1,25 @@ package com.get_offer.product.controller import ApiResponse +import com.get_offer.multipart.ImageService import com.get_offer.product.service.ProductDetailDto import com.get_offer.product.service.ProductListDto import com.get_offer.product.service.ProductService import org.springframework.data.domain.PageRequest import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/products") class ProductController( - private val productService: ProductService + private val productService: ProductService, + private val imageService: ImageService, ) { @GetMapping fun getProductList( @@ -33,4 +38,15 @@ class ProductController( fun getProductDetail(@PathVariable id: String, @RequestParam userId: String): ApiResponse { return ApiResponse.success(productService.getProductDetail(id.toLong(), userId.toLong())) } + + @PostMapping + fun postProduct( + @RequestParam userId: String, @RequestPart images: List + ) { + try { + imageService.saveImages(images) + } catch (e: Exception) { + throw e + } + } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7c5dfc7..d08d5ec 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -22,4 +22,12 @@ spring: hibernate: dialect: org.hibernate.dialect.H2Dialect format_sql: true - show_sql: true \ No newline at end of file + show_sql: true +aws: + s3: + bucket: get-offer-bucket + stack: + auto: false + credentials: + accessKey: + secretKey: \ No newline at end of file From 8cfd92a6adb754692bc2b27a998a8f3264f904b8 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Sat, 19 Oct 2024 23:06:29 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat=20:=20s3=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d08d5ec..89e2946 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -28,6 +28,4 @@ aws: bucket: get-offer-bucket stack: auto: false - credentials: - accessKey: - secretKey: \ No newline at end of file + credentials: \ No newline at end of file From efe5464aa4b9a2fea82b38417833c528799c3724 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Sat, 19 Oct 2024 14:02:17 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat=20:=20=EC=83=81=ED=92=88=20=EC=98=AC?= =?UTF-8?q?=EB=A6=AC=EA=B8=B0=20=EA=B5=AC=ED=98=84=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../get_offer/common/AuditingTimeEntity.kt | 4 +- .../product/controller/ProductController.kt | 15 ++--- .../product/controller/ProductPostReqDto.kt | 13 +++++ .../product/service/ProductSaveDto.kt | 22 ++++++++ .../product/service/ProductService.kt | 56 ++++++++++++++++++- 5 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 src/main/kotlin/com/get_offer/product/controller/ProductPostReqDto.kt create mode 100644 src/main/kotlin/com/get_offer/product/service/ProductSaveDto.kt diff --git a/src/main/kotlin/com/get_offer/common/AuditingTimeEntity.kt b/src/main/kotlin/com/get_offer/common/AuditingTimeEntity.kt index d348be2..22a0247 100644 --- a/src/main/kotlin/com/get_offer/common/AuditingTimeEntity.kt +++ b/src/main/kotlin/com/get_offer/common/AuditingTimeEntity.kt @@ -11,8 +11,8 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener @MappedSuperclass abstract class AuditingTimeEntity { @CreatedDate - private var createdAt: LocalDateTime = LocalDateTime.now() + val createdAt: LocalDateTime = LocalDateTime.now() @LastModifiedDate - private var updatedAt: LocalDateTime = LocalDateTime.now() + var updatedAt: LocalDateTime = LocalDateTime.now() } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/controller/ProductController.kt b/src/main/kotlin/com/get_offer/product/controller/ProductController.kt index af83430..f17663d 100644 --- a/src/main/kotlin/com/get_offer/product/controller/ProductController.kt +++ b/src/main/kotlin/com/get_offer/product/controller/ProductController.kt @@ -1,9 +1,9 @@ package com.get_offer.product.controller import ApiResponse -import com.get_offer.multipart.ImageService import com.get_offer.product.service.ProductDetailDto import com.get_offer.product.service.ProductListDto +import com.get_offer.product.service.ProductSaveDto import com.get_offer.product.service.ProductService import org.springframework.data.domain.PageRequest import org.springframework.web.bind.annotation.GetMapping @@ -19,7 +19,6 @@ import org.springframework.web.multipart.MultipartFile @RequestMapping("/products") class ProductController( private val productService: ProductService, - private val imageService: ImageService, ) { @GetMapping fun getProductList( @@ -41,12 +40,10 @@ class ProductController( @PostMapping fun postProduct( - @RequestParam userId: String, @RequestPart images: List - ) { - try { - imageService.saveImages(images) - } catch (e: Exception) { - throw e - } + @RequestParam userId: String, + @RequestPart("images") images: List, + @RequestPart productReqDto: ProductPostReqDto + ): ProductSaveDto { + return productService.postProduct(productReqDto, userId.toLong(), images) } } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/controller/ProductPostReqDto.kt b/src/main/kotlin/com/get_offer/product/controller/ProductPostReqDto.kt new file mode 100644 index 0000000..58f5f6e --- /dev/null +++ b/src/main/kotlin/com/get_offer/product/controller/ProductPostReqDto.kt @@ -0,0 +1,13 @@ +package com.get_offer.product.controller + +import com.get_offer.product.domain.Category +import java.time.LocalDateTime + +class ProductPostReqDto( + val title: String, + val category: Category, + val description: String, + val startPrice: Int, + val startDate: LocalDateTime, + val endDate: LocalDateTime, +) \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/service/ProductSaveDto.kt b/src/main/kotlin/com/get_offer/product/service/ProductSaveDto.kt new file mode 100644 index 0000000..2a5d412 --- /dev/null +++ b/src/main/kotlin/com/get_offer/product/service/ProductSaveDto.kt @@ -0,0 +1,22 @@ +package com.get_offer.product.service + +import com.get_offer.product.domain.Product +import java.time.LocalDateTime + +data class ProductSaveDto( + val title: String, + val id: Long, + val writerId: Long, + val createdTime: LocalDateTime +) { + companion object { + fun of(product: Product): ProductSaveDto { + return ProductSaveDto( + title = product.title, + id = product.id, + writerId = product.writerId, + createdTime = product.createdAt + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/service/ProductService.kt b/src/main/kotlin/com/get_offer/product/service/ProductService.kt index 11b3ff5..6abe000 100644 --- a/src/main/kotlin/com/get_offer/product/service/ProductService.kt +++ b/src/main/kotlin/com/get_offer/product/service/ProductService.kt @@ -1,20 +1,29 @@ package com.get_offer.product.service import com.get_offer.common.exception.NotFoundException +import com.get_offer.multipart.ImageService +import com.get_offer.product.controller.ProductPostReqDto import com.get_offer.product.domain.Product +import com.get_offer.product.domain.ProductImagesVo import com.get_offer.product.domain.ProductStatus import com.get_offer.product.repository.ProductRepository import com.get_offer.user.repository.UserRepository +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import org.apache.coyote.BadRequestException import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile @Service +@Transactional(readOnly = true) class ProductService( + private val imageService: ImageService, private val productRepository: ProductRepository, private val userRepository: UserRepository, - - ) { +) { fun getProductList(userId: Long, pageRequest: PageRequest): Page { val productList: Page = productRepository.findAllByStatusInOrderByEndDateDesc( @@ -30,4 +39,47 @@ class ProductService( return ProductDetailDto.of(product, writer, userId) } + + @Transactional + fun postProduct(req: ProductPostReqDto, userId: Long, images: List): ProductSaveDto { + + validateStartPrice(req.startPrice) + validateDateRange(req.startDate, req.endDate) + + val imageUrls = imageService.saveImages(images) + + val product = productRepository.save( + Product( + title = req.title, + category = req.category, + writerId = userId, + images = ProductImagesVo(imageUrls), + description = req.description, + startPrice = req.startPrice, + currentPrice = req.startPrice, + startDate = req.startDate, + endDate = req.endDate, + status = checkStatus(req.startDate) + ) + ) + return ProductSaveDto.of(product) + } + + private fun validateStartPrice(startPrice: Int) { + if (startPrice < 0) { + throw BadRequestException("startPrice가 0보다 작을 수 없습니다.") + } + } + + private fun validateDateRange(startDate: LocalDateTime, endDate: LocalDateTime) { + if (startDate.isAfter(endDate)) throw BadRequestException() + if (ChronoUnit.DAYS.between(startDate, endDate) > 7) throw BadRequestException() + } + + private fun checkStatus(startDate: LocalDateTime): ProductStatus { + if (startDate.isAfter(LocalDateTime.now())) { + return ProductStatus.IN_PROGRESS + } + return ProductStatus.WAIT + } } \ No newline at end of file From 0f1f8878a10cdf29278835d7a9ed1922f6521f12 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Sat, 19 Oct 2024 14:37:20 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat=20:=20=EC=97=90=EB=9F=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/get_offer/common/exception/CustomExceptions.kt | 4 +++- .../get_offer/common/exception/ExceptionControllerAdvice.kt | 5 +++++ src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt | 3 +-- src/main/kotlin/com/get_offer/multipart/FileValidate.kt | 6 +++--- .../kotlin/com/get_offer/product/service/ProductService.kt | 4 ++-- .../get_offer/product/controller/ProductIntegrationTest.kt | 5 +++++ 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/get_offer/common/exception/CustomExceptions.kt b/src/main/kotlin/com/get_offer/common/exception/CustomExceptions.kt index 60533e0..544428e 100644 --- a/src/main/kotlin/com/get_offer/common/exception/CustomExceptions.kt +++ b/src/main/kotlin/com/get_offer/common/exception/CustomExceptions.kt @@ -4,4 +4,6 @@ package com.get_offer.common.exception * custom exception 처리를 위해 남겨 놓았습니다. */ class NotFoundException(override val message: String) : RuntimeException(message) -class UnAuthorizationException : RuntimeException() \ No newline at end of file +class UnAuthorizationException : RuntimeException() + +class UnsupportedFileExtensionException : RuntimeException() \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt b/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt index ef1e911..70bb316 100644 --- a/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt +++ b/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt @@ -22,4 +22,9 @@ class ExceptionControllerAdvice { fun handleUnAuthorizationException(ex: UnAuthorizationException): ResponseEntity> { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error("인가되지 않은 사용자입니다")) } + + @ExceptionHandler + fun handleUnsupportedFileExtensionException(ex: UnsupportedFileExtensionException): ResponseEntity> { + return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).body(ApiResponse.error("파일의 확장자가 유효하지 않습니다.")) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt b/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt index b814fec..19f5a60 100644 --- a/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt +++ b/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt @@ -2,7 +2,6 @@ package com.get_offer.common.s3 import com.amazonaws.services.s3.AmazonS3 import com.amazonaws.services.s3.model.ObjectMetadata -import com.get_offer.common.exception.NotFoundException import com.get_offer.multipart.FileValidate import java.util.* import org.springframework.beans.factory.annotation.Value @@ -25,7 +24,7 @@ class S3FileManagement( fun uploadImage(multipartFile: MultipartFile): String { val originalFilename = multipartFile.originalFilename - ?: throw NotFoundException("") + ?: throw IllegalStateException() FileValidate.checkImageFormat(originalFilename) val fileName = "${UUID.randomUUID()}-${originalFilename}" val objectMetadata = setFileDateOption( diff --git a/src/main/kotlin/com/get_offer/multipart/FileValidate.kt b/src/main/kotlin/com/get_offer/multipart/FileValidate.kt index af2bbe0..27d1af0 100644 --- a/src/main/kotlin/com/get_offer/multipart/FileValidate.kt +++ b/src/main/kotlin/com/get_offer/multipart/FileValidate.kt @@ -1,6 +1,6 @@ package com.get_offer.multipart -import com.get_offer.common.exception.NotFoundException +import com.get_offer.common.exception.UnsupportedFileExtensionException class FileValidate { companion object { @@ -9,11 +9,11 @@ class FileValidate { fun checkImageFormat(fileName: String) { val extensionIndex = fileName.lastIndexOf('.') if (extensionIndex == -1) { - throw NotFoundException("") // Not exists file extension + throw UnsupportedFileExtensionException() } val extension = fileName.substring(extensionIndex + 1) require(IMAGE_EXTENSIONS.contains(extension)) { - throw NotFoundException("") // Not exists file extension + throw UnsupportedFileExtensionException() } } } diff --git a/src/main/kotlin/com/get_offer/product/service/ProductService.kt b/src/main/kotlin/com/get_offer/product/service/ProductService.kt index 6abe000..b1cc038 100644 --- a/src/main/kotlin/com/get_offer/product/service/ProductService.kt +++ b/src/main/kotlin/com/get_offer/product/service/ProductService.kt @@ -72,8 +72,8 @@ class ProductService( } private fun validateDateRange(startDate: LocalDateTime, endDate: LocalDateTime) { - if (startDate.isAfter(endDate)) throw BadRequestException() - if (ChronoUnit.DAYS.between(startDate, endDate) > 7) throw BadRequestException() + if (startDate.isAfter(endDate)) throw BadRequestException("시작 날짜가 유효하지 않습니다.") + if (ChronoUnit.DAYS.between(startDate, endDate) > 7) throw BadRequestException("경매 기간은 7일을 넘길 수 없습니다.") } private fun checkStatus(startDate: LocalDateTime): ProductStatus { diff --git a/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt b/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt index bf40769..f0d12d9 100644 --- a/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt +++ b/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt @@ -64,4 +64,9 @@ class ProductIntegrationTest( .andExpect(jsonPath("$.data.endDate").value("2024-01-04T00:00:00")) .andExpect(jsonPath("$.data.isMine").value("true")) } + + @Test + fun postProductIntegrationTest() { + + } } \ No newline at end of file From c2a6a5083b0146234f757d52631709d1f720acee Mon Sep 17 00:00:00 2001 From: pine_lee Date: Sat, 19 Oct 2024 20:47:37 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat=20:=20=EC=83=81=ED=92=88=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/controller/ProductController.kt | 4 +- .../com/get_offer/product/domain/Product.kt | 11 ++-- .../controller/ProductIntegrationTest.kt | 57 +++++++++++++----- .../product/service/ProductServiceTest.kt | 8 ++- src/test/resources/img.png | Bin 0 -> 79172 bytes src/test/resources/test_data.sql | 1 + 6 files changed, 54 insertions(+), 27 deletions(-) create mode 100644 src/test/resources/img.png diff --git a/src/main/kotlin/com/get_offer/product/controller/ProductController.kt b/src/main/kotlin/com/get_offer/product/controller/ProductController.kt index f17663d..c44f8db 100644 --- a/src/main/kotlin/com/get_offer/product/controller/ProductController.kt +++ b/src/main/kotlin/com/get_offer/product/controller/ProductController.kt @@ -43,7 +43,7 @@ class ProductController( @RequestParam userId: String, @RequestPart("images") images: List, @RequestPart productReqDto: ProductPostReqDto - ): ProductSaveDto { - return productService.postProduct(productReqDto, userId.toLong(), images) + ): ApiResponse { + return ApiResponse.success(productService.postProduct(productReqDto, userId.toLong(), images)) } } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/domain/Product.kt b/src/main/kotlin/com/get_offer/product/domain/Product.kt index 11cf6fb..cb3093a 100644 --- a/src/main/kotlin/com/get_offer/product/domain/Product.kt +++ b/src/main/kotlin/com/get_offer/product/domain/Product.kt @@ -19,11 +19,9 @@ class Product( val title: String, - @Enumerated(EnumType.STRING) - val category: Category, + @Enumerated(EnumType.STRING) val category: Category, - @Convert(converter = ProductImagesConverter::class) - @Column(name = "IMAGES") + @Convert(converter = ProductImagesConverter::class) @Column(name = "IMAGES") val images: ProductImagesVo, val description: String, @@ -39,7 +37,6 @@ class Product( var endDate: LocalDateTime, - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long = 0L, + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long = 0L, ) : AuditingTimeEntity() \ No newline at end of file diff --git a/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt b/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt index f0d12d9..c4b714a 100644 --- a/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt +++ b/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt @@ -1,11 +1,16 @@ package com.get_offer.product.controller +import java.nio.file.Files +import java.nio.file.Paths import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType +import org.springframework.mock.web.MockMultipartFile import org.springframework.test.context.jdbc.Sql import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.result.MockMvcResultHandlers import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath @@ -21,15 +26,10 @@ class ProductIntegrationTest( @Test fun productListIntegrationTest() { mockMvc.perform( - get("/products") - .param("userId", "1") - .param("page", "0") - .param("size", "30") + get("/products").param("userId", "1").param("page", "0").param("size", "30") ).andDo(MockMvcResultHandlers.print()).andExpect(status().isOk) - .andExpect(jsonPath("$.data.pageNumber").value(0)) - .andExpect(jsonPath("$.data.pageSize").value(30)) - .andExpect(jsonPath("$.data.totalElements").value(2)) - .andExpect(jsonPath("$.data.totalPages").value(1)) + .andExpect(jsonPath("$.data.pageNumber").value(0)).andExpect(jsonPath("$.data.pageSize").value(30)) + .andExpect(jsonPath("$.data.totalElements").value(2)).andExpect(jsonPath("$.data.totalPages").value(1)) .andExpect(jsonPath("$.data.content[0].id").value("1")) .andExpect(jsonPath("$.data.content[0].writerId").value("1")) .andExpect(jsonPath("$.data.content[0].name").value("nintendo")) @@ -46,14 +46,11 @@ class ProductIntegrationTest( fun productDetailIntegrationTest() { mockMvc.perform( get("/products/1/detail").param("userId", "1") - ).andDo(MockMvcResultHandlers.print()).andExpect(status().isOk) - .andExpect(jsonPath("$.data.id").value("1")) - .andExpect(jsonPath("$.data.name").value("nintendo")) - .andExpect(jsonPath("$.data.writer.id").value("1")) + ).andDo(MockMvcResultHandlers.print()).andExpect(status().isOk).andExpect(jsonPath("$.data.id").value("1")) + .andExpect(jsonPath("$.data.name").value("nintendo")).andExpect(jsonPath("$.data.writer.id").value("1")) .andExpect(jsonPath("$.data.writer.nickname").value("test")) .andExpect(jsonPath("$.data.writer.profileImg").value("https://drive.google.com/file/d/1R9EIOoEWWgPUhY6e-t4VFuqMgknl7rm8/view?usp=sharing")) - .andExpect(jsonPath("$.data.name").value("nintendo")) - .andExpect(jsonPath("$.data.category").value("GAMES")) + .andExpect(jsonPath("$.data.name").value("nintendo")).andExpect(jsonPath("$.data.category").value("GAMES")) .andExpect(jsonPath("$.data.images.size()").value(2)) .andExpect(jsonPath("$.data.images[0]").value("https://picsum.photos/200/300")) .andExpect(jsonPath("$.data.description").value("닌텐도 새 제품")) @@ -64,9 +61,39 @@ class ProductIntegrationTest( .andExpect(jsonPath("$.data.endDate").value("2024-01-04T00:00:00")) .andExpect(jsonPath("$.data.isMine").value("true")) } - + @Test fun postProductIntegrationTest() { + // 이미지 파일 로드 + val imagePath = Paths.get("src/test/resources/img.png") + val imageFile = MockMultipartFile( + "images", "img.png", MediaType.IMAGE_PNG_VALUE, Files.readAllBytes(imagePath) + ) + // JSON 데이터 생성 + val productReqDto = """ + { + "title": "솔 타이틀", + "description": "설명", + "startPrice": 1000, + "startDate": "2024-10-19T15:00:00", + "endDate": "2024-10-23T15:00:00", + "category": "BOOKS" + } + """.trimIndent() + val productReqDtoFile = MockMultipartFile( + "productReqDto", "productReqDto", MediaType.APPLICATION_JSON_VALUE, productReqDto.toByteArray() + ) + + // MockMvc 요청 작성 및 실행 + mockMvc.perform( + MockMvcRequestBuilders.multipart("/products") + .file(imageFile) + .file(productReqDtoFile) + .param("userId", "1") + .contentType(MediaType.MULTIPART_FORM_DATA) + ).andExpect(status().isOk) + .andExpect(jsonPath("$.data.title").value("솔 타이틀")) + .andExpect(jsonPath("$.data.writerId").value(1)) } } \ No newline at end of file diff --git a/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt b/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt index bc7f9e2..5a16787 100644 --- a/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt +++ b/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt @@ -1,6 +1,7 @@ package com.get_offer.product.service import com.get_offer.TestFixtures +import com.get_offer.multipart.ImageService import com.get_offer.product.domain.ProductStatus import com.get_offer.product.repository.ProductRepository import com.get_offer.user.domain.User @@ -21,12 +22,14 @@ class ProductServiceTest { private lateinit var productService: ProductService private lateinit var mockProductRepository: ProductRepository private lateinit var mockUserRepository: UserRepository + private lateinit var mockImageService: ImageService @BeforeEach fun setUp() { mockProductRepository = mock(ProductRepository::class.java) mockUserRepository = mock(UserRepository::class.java) - productService = ProductService(mockProductRepository, mockUserRepository) + mockImageService = mock(ImageService::class.java) + productService = ProductService(mockImageService, mockProductRepository, mockUserRepository) } @Test @@ -41,8 +44,7 @@ class ProductServiceTest { `when`( mockProductRepository.findAllByStatusInOrderByEndDateDesc( - listOf(ProductStatus.IN_PROGRESS, ProductStatus.WAIT), - pageable + listOf(ProductStatus.IN_PROGRESS, ProductStatus.WAIT), pageable ) ).thenReturn( PageImpl(items, pageable, items.size.toLong()) diff --git a/src/test/resources/img.png b/src/test/resources/img.png new file mode 100644 index 0000000000000000000000000000000000000000..0a1f2585007331216f12c06d26ec2e31109a4127 GIT binary patch literal 79172 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 f|9<;ly9U=3+EttTf5`{cA>c<4jKMm6`Dzx literal 0 HcmV?d00001 diff --git a/src/test/resources/test_data.sql b/src/test/resources/test_data.sql index dfb52f9..86f9671 100644 --- a/src/test/resources/test_data.sql +++ b/src/test/resources/test_data.sql @@ -26,6 +26,7 @@ INSERT INTO PRODUCTS (ID, WRITER_ID, TITLE, DESCRIPTION, CATEGORY, CURRENT_PRICE VALUES (3, 1, 'ikea chair', '저쩌', 'FURNITURE', 43000, 8000, 'COMPLETED', '2024-01-06 00:00:00', '2024-01-04 00:00:00', '2024-01-02 00:00:00', '2024-01-02 00:00:00', '{"images":["https://picsum.photos/200/300","https://picsum.photos/200/300"]}'); +ALTER TABLE PRODUCTS ALTER COLUMN ID RESTART WITH 4; INSERT INTO AUCTION_RESULTS (ID, PRODUCT_ID, BUYER_ID, FINAL_PRICE, AUCTION_STATUS, CREATED_AT, UPDATED_AT) From 350e80daca2d152c4c23706f183a571f2ee1e9e3 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Sat, 19 Oct 2024 20:55:24 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat=20:=20=EC=83=81=ED=92=88=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=9C=A0=EB=8B=9B=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/service/ProductServiceTest.kt | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt b/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt index 5a16787..109e76c 100644 --- a/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt +++ b/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt @@ -2,21 +2,29 @@ package com.get_offer.product.service import com.get_offer.TestFixtures import com.get_offer.multipart.ImageService +import com.get_offer.product.controller.ProductPostReqDto +import com.get_offer.product.domain.Category +import com.get_offer.product.domain.Product +import com.get_offer.product.domain.ProductImagesVo import com.get_offer.product.domain.ProductStatus import com.get_offer.product.repository.ProductRepository import com.get_offer.user.domain.User import com.get_offer.user.repository.UserRepository +import java.time.LocalDateTime import java.util.* +import org.apache.coyote.BadRequestException import org.assertj.core.api.Assertions import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.anyList import org.mockito.Mockito.any import org.mockito.Mockito.mock import org.mockito.Mockito.`when` import org.springframework.data.domain.PageImpl import org.springframework.data.domain.PageRequest +import org.springframework.mock.web.MockMultipartFile class ProductServiceTest { private lateinit var productService: ProductService @@ -82,4 +90,92 @@ class ProductServiceTest { Assertions.assertThat(result.images[0]).isEqualTo("https://image1.png") Assertions.assertThat(result.images[1]).isEqualTo("https://image2.png") } + + @Test + fun postProductWithValidData() { + // given + val userId = 1L + val productReqDto = ProductPostReqDto( + title = "Test Product", + description = "Test Description", + startPrice = 1000, + startDate = LocalDateTime.now().plusDays(1), + endDate = LocalDateTime.now().plusDays(3), + category = Category.BOOKS + ) + + val mockImage = MockMultipartFile("images", "test.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) + + val imageUrls = listOf("http://image-url.com/test.jpg") + `when`(mockImageService.saveImages(anyList())).thenReturn(imageUrls) + + val product = Product( + title = productReqDto.title, + category = productReqDto.category, + writerId = userId, + images = ProductImagesVo(imageUrls), + description = productReqDto.description, + startPrice = productReqDto.startPrice, + currentPrice = productReqDto.startPrice, + startDate = productReqDto.startDate, + endDate = productReqDto.endDate, + status = ProductStatus.IN_PROGRESS + ) + + `when`(mockProductRepository.save(any(Product::class.java))).thenReturn(product) + + // when + val result = productService.postProduct(productReqDto, userId, listOf(mockImage)) + + // then + assertNotNull(result) + assertEquals(productReqDto.title, result.title) + } + + @Test + fun postProductWithInvalidStartPrice() { + // given + val userId = 1L + val productReqDto = ProductPostReqDto( + title = "Test Product", + description = "Test Description", + startPrice = -1000, // Invalid start price + startDate = LocalDateTime.now().plusDays(1), + endDate = LocalDateTime.now().plusDays(3), + category = Category.BOOKS + ) + + val mockImage = MockMultipartFile("images", "test.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) + + // when & then + val exception = assertThrows(BadRequestException::class.java) { + productService.postProduct(productReqDto, userId, listOf(mockImage)) + } + + assertEquals("startPrice가 0보다 작을 수 없습니다.", exception.message) + } + + @Test + fun `test postProduct with invalid date range`() { + // given + val userId = 1L + val productReqDto = ProductPostReqDto( + title = "Test Product", + description = "Test Description", + startPrice = 1000, + startDate = LocalDateTime.now().plusDays(10), // Invalid start date (after end date) + endDate = LocalDateTime.now().plusDays(3), + category = Category.BOOKS + ) + + val mockImage = MockMultipartFile("images", "test.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) + + // when & then + val exception = assertThrows(BadRequestException::class.java) { + productService.postProduct(productReqDto, userId, listOf(mockImage)) + } + + assertEquals("시작 날짜가 유효하지 않습니다.", exception.message) + } + } \ No newline at end of file From 6acc5d5296b7d5e76ef43ea2c1ec7514eb71b45e Mon Sep 17 00:00:00 2001 From: pine_lee Date: Sun, 20 Oct 2024 00:17:04 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat=20:=20=EC=83=81=ED=92=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20api=20=EA=B5=AC=ED=98=84=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../get_offer/common/s3/S3FileManagement.kt | 4 ++ .../com/get_offer/multipart/ImageService.kt | 4 +- .../product/controller/ProductController.kt | 16 ++++++ .../product/controller/ProductEditReqDto.kt | 13 +++++ .../product/controller/ProductPostReqDto.kt | 2 +- .../com/get_offer/product/domain/Product.kt | 52 ++++++++++++++++++- .../product/domain/ProductEditReq.kt | 32 ++++++++++++ .../product/service/ProductEditDto.kt | 34 ++++++++++++ .../product/service/ProductService.kt | 43 ++++++++------- 9 files changed, 176 insertions(+), 24 deletions(-) create mode 100644 src/main/kotlin/com/get_offer/product/controller/ProductEditReqDto.kt create mode 100644 src/main/kotlin/com/get_offer/product/domain/ProductEditReq.kt create mode 100644 src/main/kotlin/com/get_offer/product/service/ProductEditDto.kt diff --git a/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt b/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt index 19f5a60..160de2f 100644 --- a/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt +++ b/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt @@ -40,6 +40,10 @@ class S3FileManagement( return amazonS3.getUrl(bucket, fileName).toString() } + fun delete(fileNames: List) { + fileNames.forEach { delete(it) } + } + fun delete(fileName: String) { amazonS3.deleteObject(bucket, fileName) } diff --git a/src/main/kotlin/com/get_offer/multipart/ImageService.kt b/src/main/kotlin/com/get_offer/multipart/ImageService.kt index 4b8da74..239c9bd 100644 --- a/src/main/kotlin/com/get_offer/multipart/ImageService.kt +++ b/src/main/kotlin/com/get_offer/multipart/ImageService.kt @@ -12,7 +12,7 @@ class ImageService( return s3FileManagement.uploadImages(images) } - fun deleteImage(imageUrl: String) { - val file = s3FileManagement.delete(imageUrl) + fun deleteImages(imageUrls: List) { + val file = s3FileManagement.delete(imageUrls) } } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/controller/ProductController.kt b/src/main/kotlin/com/get_offer/product/controller/ProductController.kt index c44f8db..b9d7ec8 100644 --- a/src/main/kotlin/com/get_offer/product/controller/ProductController.kt +++ b/src/main/kotlin/com/get_offer/product/controller/ProductController.kt @@ -2,11 +2,13 @@ package com.get_offer.product.controller import ApiResponse import com.get_offer.product.service.ProductDetailDto +import com.get_offer.product.service.ProductEditDto import com.get_offer.product.service.ProductListDto import com.get_offer.product.service.ProductSaveDto import com.get_offer.product.service.ProductService import org.springframework.data.domain.PageRequest import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping @@ -46,4 +48,18 @@ class ProductController( ): ApiResponse { return ApiResponse.success(productService.postProduct(productReqDto, userId.toLong(), images)) } + + @PatchMapping("{productId}") + fun editProduct( + @PathVariable productId: Long, + @RequestParam userId: String, + @RequestPart("images") images: List?, + @RequestPart productReqDto: ProductEditReqDto + ): ApiResponse { + return ApiResponse.success( + productService.editProduct( + ProductEditDto.of(productId, productReqDto, userId.toLong(), images) + ) + ) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/controller/ProductEditReqDto.kt b/src/main/kotlin/com/get_offer/product/controller/ProductEditReqDto.kt new file mode 100644 index 0000000..3540a63 --- /dev/null +++ b/src/main/kotlin/com/get_offer/product/controller/ProductEditReqDto.kt @@ -0,0 +1,13 @@ +package com.get_offer.product.controller + +import com.get_offer.product.domain.Category +import java.time.LocalDateTime + +data class ProductEditReqDto( + val title: String?, + val category: Category?, + val description: String?, + val startPrice: Int?, + val startDate: LocalDateTime?, + val endDate: LocalDateTime?, +) \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/controller/ProductPostReqDto.kt b/src/main/kotlin/com/get_offer/product/controller/ProductPostReqDto.kt index 58f5f6e..48dbd1d 100644 --- a/src/main/kotlin/com/get_offer/product/controller/ProductPostReqDto.kt +++ b/src/main/kotlin/com/get_offer/product/controller/ProductPostReqDto.kt @@ -3,7 +3,7 @@ package com.get_offer.product.controller import com.get_offer.product.domain.Category import java.time.LocalDateTime -class ProductPostReqDto( +data class ProductPostReqDto( val title: String, val category: Category, val description: String, diff --git a/src/main/kotlin/com/get_offer/product/domain/Product.kt b/src/main/kotlin/com/get_offer/product/domain/Product.kt index cb3093a..704d058 100644 --- a/src/main/kotlin/com/get_offer/product/domain/Product.kt +++ b/src/main/kotlin/com/get_offer/product/domain/Product.kt @@ -11,6 +11,8 @@ import jakarta.persistence.GenerationType import jakarta.persistence.Id import jakarta.persistence.Table import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import org.apache.coyote.BadRequestException @Entity @Table(name = "PRODUCTS") @@ -39,4 +41,52 @@ class Product( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = 0L, -) : AuditingTimeEntity() \ No newline at end of file +) : AuditingTimeEntity() { + fun updateProduct(dto: ProductEditReq): Product { + if (dto.startPrice != null) { + validateStartPrice(dto.startPrice) + } + + val startDate = dto.startDate ?: this.startDate + val endDate = dto.endDate ?: this.endDate + validateDateRange(startDate, endDate) + + return Product( + title = dto.title ?: this.title, + description = dto.description ?: this.description, + startPrice = dto.startPrice ?: this.startPrice, + currentPrice = dto.startPrice ?: this.startPrice, + category = dto.category ?: this.category, + status = ProductStatus.WAIT, + startDate = startDate, + endDate = endDate, + images = dto.images?.let { ProductImagesVo(it) } ?: this.images, + writerId = this.writerId + ) + } + + companion object { + fun validateProduct(startPrice: Int, startDate: LocalDateTime, endDate: LocalDateTime) { + validateStartPrice(startPrice) + validateDateRange(startDate, endDate) + } + + fun checkStatus(startDate: LocalDateTime): ProductStatus { + if (startDate.isAfter(LocalDateTime.now())) { + return ProductStatus.IN_PROGRESS + } + return ProductStatus.WAIT + } + + private fun validateStartPrice(startPrice: Int) { + if (startPrice < 0) { + throw BadRequestException("startPrice가 0보다 작을 수 없습니다.") + } + } + + private fun validateDateRange(startDate: LocalDateTime, endDate: LocalDateTime) { + if (startDate.isAfter(endDate)) throw BadRequestException("시작 날짜가 유효하지 않습니다.") + if (ChronoUnit.DAYS.between(startDate, endDate) > 7) throw BadRequestException("경매 기간은 7일을 넘길 수 없습니다.") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/domain/ProductEditReq.kt b/src/main/kotlin/com/get_offer/product/domain/ProductEditReq.kt new file mode 100644 index 0000000..31e95c2 --- /dev/null +++ b/src/main/kotlin/com/get_offer/product/domain/ProductEditReq.kt @@ -0,0 +1,32 @@ +package com.get_offer.product.domain + +import com.get_offer.product.service.ProductEditDto +import java.time.LocalDateTime + +data class ProductEditReq( + val productId: Long, + val title: String?, + val category: Category?, + val description: String?, + val startPrice: Int?, + val startDate: LocalDateTime?, + val endDate: LocalDateTime?, + val writerId: Long, + val images: List? +) { + companion object { + fun of(req: ProductEditDto, imageUrls: List?): ProductEditReq { + return ProductEditReq( + productId = req.productId, + title = req.title, + category = req.category, + description = req.description, + startPrice = req.startPrice, + startDate = req.startDate, + endDate = req.endDate, + writerId = req.writerId, + images = imageUrls + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/service/ProductEditDto.kt b/src/main/kotlin/com/get_offer/product/service/ProductEditDto.kt new file mode 100644 index 0000000..4e76311 --- /dev/null +++ b/src/main/kotlin/com/get_offer/product/service/ProductEditDto.kt @@ -0,0 +1,34 @@ +package com.get_offer.product.service + +import com.get_offer.product.controller.ProductEditReqDto +import com.get_offer.product.domain.Category +import java.time.LocalDateTime +import org.springframework.web.multipart.MultipartFile + +data class ProductEditDto( + val productId: Long, + val title: String?, + val category: Category?, + val description: String?, + val startPrice: Int?, + val startDate: LocalDateTime?, + val endDate: LocalDateTime?, + val writerId: Long, + val images: List? +) { + companion object { + fun of(productId: Long, req: ProductEditReqDto, userId: Long, images: List?): ProductEditDto { + return ProductEditDto( + productId = productId, + title = req.title, + category = req.category, + description = req.description, + startPrice = req.startPrice, + startDate = req.startDate, + endDate = req.endDate, + writerId = userId, + images = images + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/service/ProductService.kt b/src/main/kotlin/com/get_offer/product/service/ProductService.kt index b1cc038..2b3f678 100644 --- a/src/main/kotlin/com/get_offer/product/service/ProductService.kt +++ b/src/main/kotlin/com/get_offer/product/service/ProductService.kt @@ -1,15 +1,15 @@ package com.get_offer.product.service import com.get_offer.common.exception.NotFoundException +import com.get_offer.common.exception.UnAuthorizationException import com.get_offer.multipart.ImageService import com.get_offer.product.controller.ProductPostReqDto import com.get_offer.product.domain.Product +import com.get_offer.product.domain.ProductEditReq import com.get_offer.product.domain.ProductImagesVo import com.get_offer.product.domain.ProductStatus import com.get_offer.product.repository.ProductRepository import com.get_offer.user.repository.UserRepository -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit import org.apache.coyote.BadRequestException import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest @@ -42,12 +42,10 @@ class ProductService( @Transactional fun postProduct(req: ProductPostReqDto, userId: Long, images: List): ProductSaveDto { - - validateStartPrice(req.startPrice) - validateDateRange(req.startDate, req.endDate) - val imageUrls = imageService.saveImages(images) + Product.validateProduct(req.startPrice, req.startDate, req.endDate) + val product = productRepository.save( Product( title = req.title, @@ -59,27 +57,32 @@ class ProductService( currentPrice = req.startPrice, startDate = req.startDate, endDate = req.endDate, - status = checkStatus(req.startDate) + status = Product.checkStatus(req.startDate) ) ) return ProductSaveDto.of(product) } - private fun validateStartPrice(startPrice: Int) { - if (startPrice < 0) { - throw BadRequestException("startPrice가 0보다 작을 수 없습니다.") + @Transactional + fun editProduct(req: ProductEditDto): ProductSaveDto { + val product = productRepository.findById(req.productId) + .orElseThrow { NotFoundException("${req.productId} 의 상품은 존재하지 않습니다.") } + // access + if (product.writerId != req.writerId) { + throw UnAuthorizationException() } - } - private fun validateDateRange(startDate: LocalDateTime, endDate: LocalDateTime) { - if (startDate.isAfter(endDate)) throw BadRequestException("시작 날짜가 유효하지 않습니다.") - if (ChronoUnit.DAYS.between(startDate, endDate) > 7) throw BadRequestException("경매 기간은 7일을 넘길 수 없습니다.") - } - - private fun checkStatus(startDate: LocalDateTime): ProductStatus { - if (startDate.isAfter(LocalDateTime.now())) { - return ProductStatus.IN_PROGRESS + if (product.status != ProductStatus.WAIT) { + throw BadRequestException("진행 전 대기 상태에서만 수정 할 수 있습니다.") } - return ProductStatus.WAIT + + val imageUrls = if (req.images != null) { + imageService.deleteImages(product.images.images) // 기존 사진 삭제 + imageService.saveImages(req.images) + } else null + + product.updateProduct(ProductEditReq.of(req, imageUrls)) + + return ProductSaveDto.of(product) } } \ No newline at end of file From e1ca3bc6454746863bd4fd6ec0d87040e8196f87 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Sun, 20 Oct 2024 00:49:49 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat=20:=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/ExceptionControllerAdvice.kt | 6 ++++++ .../com/get_offer/multipart/FileValidate.kt | 2 +- .../com/get_offer/product/domain/Product.kt | 18 ++++++++---------- .../product/service/ProductService.kt | 1 + 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt b/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt index 70bb316..d1aa7e9 100644 --- a/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt +++ b/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt @@ -1,6 +1,7 @@ package com.get_offer.common.exception import ApiResponse +import org.apache.coyote.BadRequestException import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ExceptionHandler @@ -13,6 +14,11 @@ class ExceptionControllerAdvice { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.error(ex.message ?: "DEFAULT ERROR")) } + @ExceptionHandler + fun handleBadRequestException(ex: BadRequestException): ResponseEntity> { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.error(ex.message ?: "DEFAULT ERROR")) + } + @ExceptionHandler fun handleNotFoundException(ex: NotFoundException): ResponseEntity> { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.error(ex.message)) diff --git a/src/main/kotlin/com/get_offer/multipart/FileValidate.kt b/src/main/kotlin/com/get_offer/multipart/FileValidate.kt index 27d1af0..dc37f33 100644 --- a/src/main/kotlin/com/get_offer/multipart/FileValidate.kt +++ b/src/main/kotlin/com/get_offer/multipart/FileValidate.kt @@ -4,7 +4,7 @@ import com.get_offer.common.exception.UnsupportedFileExtensionException class FileValidate { companion object { - private val IMAGE_EXTENSIONS: List = listOf("jpg", "png") + private val IMAGE_EXTENSIONS: List = listOf("jpg", "png", "jpeg") fun checkImageFormat(fileName: String) { val extensionIndex = fileName.lastIndexOf('.') diff --git a/src/main/kotlin/com/get_offer/product/domain/Product.kt b/src/main/kotlin/com/get_offer/product/domain/Product.kt index 704d058..e107552 100644 --- a/src/main/kotlin/com/get_offer/product/domain/Product.kt +++ b/src/main/kotlin/com/get_offer/product/domain/Product.kt @@ -21,10 +21,10 @@ class Product( val title: String, - @Enumerated(EnumType.STRING) val category: Category, + @Enumerated(EnumType.STRING) + val category: Category, - @Convert(converter = ProductImagesConverter::class) @Column(name = "IMAGES") - val images: ProductImagesVo, + @Convert(converter = ProductImagesConverter::class) @Column(name = "IMAGES") val images: ProductImagesVo, val description: String, @@ -32,15 +32,13 @@ class Product( var currentPrice: Int, - @Enumerated(EnumType.STRING) - var status: ProductStatus, + @Enumerated(EnumType.STRING) var status: ProductStatus, var startDate: LocalDateTime, var endDate: LocalDateTime, - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - var id: Long = 0L, + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = 0L, ) : AuditingTimeEntity() { fun updateProduct(dto: ProductEditReq): Product { if (dto.startPrice != null) { @@ -72,10 +70,10 @@ class Product( } fun checkStatus(startDate: LocalDateTime): ProductStatus { - if (startDate.isAfter(LocalDateTime.now())) { - return ProductStatus.IN_PROGRESS + if (LocalDateTime.now().isBefore(startDate)) { + return ProductStatus.WAIT } - return ProductStatus.WAIT + return ProductStatus.IN_PROGRESS } private fun validateStartPrice(startPrice: Int) { diff --git a/src/main/kotlin/com/get_offer/product/service/ProductService.kt b/src/main/kotlin/com/get_offer/product/service/ProductService.kt index 2b3f678..7c77856 100644 --- a/src/main/kotlin/com/get_offer/product/service/ProductService.kt +++ b/src/main/kotlin/com/get_offer/product/service/ProductService.kt @@ -82,6 +82,7 @@ class ProductService( } else null product.updateProduct(ProductEditReq.of(req, imageUrls)) + productRepository.save(product) return ProductSaveDto.of(product) } From e39fdbbc81c92b9e6ba3eed3def951f94e8e1190 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Sun, 20 Oct 2024 01:07:36 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/service/ProductEditDto.kt | 14 +++---- .../controller/ProductIntegrationTest.kt | 39 ++++++++++++++++-- .../product/service/ProductServiceTest.kt | 29 +++++++++++++ src/test/resources/img_1.png | Bin 0 -> 64174 bytes 4 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 src/test/resources/img_1.png diff --git a/src/main/kotlin/com/get_offer/product/service/ProductEditDto.kt b/src/main/kotlin/com/get_offer/product/service/ProductEditDto.kt index 4e76311..793527d 100644 --- a/src/main/kotlin/com/get_offer/product/service/ProductEditDto.kt +++ b/src/main/kotlin/com/get_offer/product/service/ProductEditDto.kt @@ -7,14 +7,14 @@ import org.springframework.web.multipart.MultipartFile data class ProductEditDto( val productId: Long, - val title: String?, - val category: Category?, - val description: String?, - val startPrice: Int?, - val startDate: LocalDateTime?, - val endDate: LocalDateTime?, val writerId: Long, - val images: List? + val title: String? = null, + val category: Category? = null, + val description: String? = null, + val startPrice: Int? = null, + val startDate: LocalDateTime? = null, + val endDate: LocalDateTime? = null, + val images: List? = null ) { companion object { fun of(productId: Long, req: ProductEditReqDto, userId: Long, images: List?): ProductEditDto { diff --git a/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt b/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt index c4b714a..647c89b 100644 --- a/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt +++ b/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt @@ -61,7 +61,7 @@ class ProductIntegrationTest( .andExpect(jsonPath("$.data.endDate").value("2024-01-04T00:00:00")) .andExpect(jsonPath("$.data.isMine").value("true")) } - + @Test fun postProductIntegrationTest() { // 이미지 파일 로드 @@ -76,8 +76,8 @@ class ProductIntegrationTest( "title": "솔 타이틀", "description": "설명", "startPrice": 1000, - "startDate": "2024-10-19T15:00:00", - "endDate": "2024-10-23T15:00:00", + "startDate": "2099-10-19T15:00:00", + "endDate": "2099-10-23T15:00:00", "category": "BOOKS" } """.trimIndent() @@ -96,4 +96,37 @@ class ProductIntegrationTest( .andExpect(jsonPath("$.data.title").value("솔 타이틀")) .andExpect(jsonPath("$.data.writerId").value(1)) } + + @Test + fun updateProductIntegrationTest() { + // 이미지 파일 로드 + val imagePath = Paths.get("src/test/resources/img_1.png") + val imageFile = MockMultipartFile( + "images", "img_1.png", MediaType.IMAGE_PNG_VALUE, Files.readAllBytes(imagePath) + ) + + // JSON 데이터 생성 + val productReqDto = """ + { + "title": "수정된 제목", + "description": "수정된 설명", + "startPrice": 1500, + "category": "GAMES" + } + """.trimIndent() + val productReqDtoFile = MockMultipartFile( + "productReqDto", "productReqDto", MediaType.APPLICATION_JSON_VALUE, productReqDto.toByteArray() + ) + + // MockMvc 요청 작성 및 실행 + mockMvc.perform( + MockMvcRequestBuilders.multipart("/products/1") + .file(imageFile) + .file(productReqDtoFile) + .param("userId", "1") + .contentType(MediaType.MULTIPART_FORM_DATA) + ).andExpect(status().isOk) + .andExpect(jsonPath("$.data.title").value("수정된 제목")) + .andExpect(jsonPath("$.data.writerId").value(1)) + } } \ No newline at end of file diff --git a/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt b/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt index 109e76c..7a9e6e4 100644 --- a/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt +++ b/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt @@ -178,4 +178,33 @@ class ProductServiceTest { assertEquals("시작 날짜가 유효하지 않습니다.", exception.message) } + @Test + fun editProductReturnsDto() { + // Given + val productId = 1L + val writerId = 1L + val req = ProductEditDto( + productId = productId, + writerId = writerId, + title = "new title", + description = "new description" + ) + val existingProduct = Product( + writerId, "old Title", Category.BOOKS, images = ProductImagesVo(listOf("images.png")), + "old description", 1000, 1000, ProductStatus.WAIT, + LocalDateTime.now(), + LocalDateTime.now(), + productId, + ) + + `when`(mockProductRepository.findById(productId)).thenReturn(Optional.of(existingProduct)) + `when`(mockProductRepository.save(any(Product::class.java))).thenReturn(existingProduct) + + // When + val result = productService.editProduct(req) + + // Then + assertEquals(req.title, result.title) + assertEquals(req.writerId, result.writerId) + } } \ No newline at end of file diff --git a/src/test/resources/img_1.png b/src/test/resources/img_1.png new file mode 100644 index 0000000000000000000000000000000000000000..31b284023492b1223278d16821a0003688f9fda2 GIT binary patch literal 64174 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 literal 0 HcmV?d00001 From ad1c62cec13669a10ff4a162fb551ebcfe00ea34 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Mon, 21 Oct 2024 09:31:33 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/get_offer/multipart/ImageService.kt | 2 +- .../product/controller/ProductController.kt | 4 +- .../com/get_offer/product/domain/Product.kt | 3 +- .../product/service/ProductService.kt | 5 ++- .../controller/ProductIntegrationTest.kt | 40 +++++++++---------- .../product/service/ProductServiceTest.kt | 37 ++++++++--------- 6 files changed, 43 insertions(+), 48 deletions(-) diff --git a/src/main/kotlin/com/get_offer/multipart/ImageService.kt b/src/main/kotlin/com/get_offer/multipart/ImageService.kt index 239c9bd..40a54fe 100644 --- a/src/main/kotlin/com/get_offer/multipart/ImageService.kt +++ b/src/main/kotlin/com/get_offer/multipart/ImageService.kt @@ -13,6 +13,6 @@ class ImageService( } fun deleteImages(imageUrls: List) { - val file = s3FileManagement.delete(imageUrls) + s3FileManagement.delete(imageUrls) } } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/product/controller/ProductController.kt b/src/main/kotlin/com/get_offer/product/controller/ProductController.kt index b9d7ec8..665a9e8 100644 --- a/src/main/kotlin/com/get_offer/product/controller/ProductController.kt +++ b/src/main/kotlin/com/get_offer/product/controller/ProductController.kt @@ -8,9 +8,9 @@ import com.get_offer.product.service.ProductSaveDto import com.get_offer.product.service.ProductService import org.springframework.data.domain.PageRequest import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestPart @@ -49,7 +49,7 @@ class ProductController( return ApiResponse.success(productService.postProduct(productReqDto, userId.toLong(), images)) } - @PatchMapping("{productId}") + @PutMapping("{productId}") fun editProduct( @PathVariable productId: Long, @RequestParam userId: String, diff --git a/src/main/kotlin/com/get_offer/product/domain/Product.kt b/src/main/kotlin/com/get_offer/product/domain/Product.kt index e107552..489d2ca 100644 --- a/src/main/kotlin/com/get_offer/product/domain/Product.kt +++ b/src/main/kotlin/com/get_offer/product/domain/Product.kt @@ -50,6 +50,7 @@ class Product( validateDateRange(startDate, endDate) return Product( + id = this.id, title = dto.title ?: this.title, description = dto.description ?: this.description, startPrice = dto.startPrice ?: this.startPrice, @@ -59,7 +60,7 @@ class Product( startDate = startDate, endDate = endDate, images = dto.images?.let { ProductImagesVo(it) } ?: this.images, - writerId = this.writerId + writerId = this.writerId, ) } diff --git a/src/main/kotlin/com/get_offer/product/service/ProductService.kt b/src/main/kotlin/com/get_offer/product/service/ProductService.kt index 7c77856..a02239f 100644 --- a/src/main/kotlin/com/get_offer/product/service/ProductService.kt +++ b/src/main/kotlin/com/get_offer/product/service/ProductService.kt @@ -65,7 +65,7 @@ class ProductService( @Transactional fun editProduct(req: ProductEditDto): ProductSaveDto { - val product = productRepository.findById(req.productId) + var product = productRepository.findById(req.productId) .orElseThrow { NotFoundException("${req.productId} 의 상품은 존재하지 않습니다.") } // access if (product.writerId != req.writerId) { @@ -81,7 +81,8 @@ class ProductService( imageService.saveImages(req.images) } else null - product.updateProduct(ProductEditReq.of(req, imageUrls)) + + product = product.updateProduct(ProductEditReq.of(req, imageUrls)) productRepository.save(product) return ProductSaveDto.of(product) diff --git a/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt b/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt index 647c89b..7e9a829 100644 --- a/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt +++ b/src/test/kotlin/com/get_offer/product/controller/ProductIntegrationTest.kt @@ -65,10 +65,7 @@ class ProductIntegrationTest( @Test fun postProductIntegrationTest() { // 이미지 파일 로드 - val imagePath = Paths.get("src/test/resources/img.png") - val imageFile = MockMultipartFile( - "images", "img.png", MediaType.IMAGE_PNG_VALUE, Files.readAllBytes(imagePath) - ) + val imageFile = loadImageFile("img.png") // JSON 데이터 생성 val productReqDto = """ @@ -87,23 +84,16 @@ class ProductIntegrationTest( // MockMvc 요청 작성 및 실행 mockMvc.perform( - MockMvcRequestBuilders.multipart("/products") - .file(imageFile) - .file(productReqDtoFile) - .param("userId", "1") + MockMvcRequestBuilders.multipart("/products").file(imageFile).file(productReqDtoFile).param("userId", "1") .contentType(MediaType.MULTIPART_FORM_DATA) - ).andExpect(status().isOk) - .andExpect(jsonPath("$.data.title").value("솔 타이틀")) + ).andExpect(status().isOk).andExpect(jsonPath("$.data.title").value("솔 타이틀")) .andExpect(jsonPath("$.data.writerId").value(1)) } @Test fun updateProductIntegrationTest() { // 이미지 파일 로드 - val imagePath = Paths.get("src/test/resources/img_1.png") - val imageFile = MockMultipartFile( - "images", "img_1.png", MediaType.IMAGE_PNG_VALUE, Files.readAllBytes(imagePath) - ) + val imageFile = loadImageFile("img_1.png") // JSON 데이터 생성 val productReqDto = """ @@ -113,20 +103,28 @@ class ProductIntegrationTest( "startPrice": 1500, "category": "GAMES" } - """.trimIndent() + """.trimIndent() val productReqDtoFile = MockMultipartFile( "productReqDto", "productReqDto", MediaType.APPLICATION_JSON_VALUE, productReqDto.toByteArray() ) + val builder = MockMvcRequestBuilders.multipart("/products/2") + builder.with { request -> + request.method = "PUT" + request + } // MockMvc 요청 작성 및 실행 mockMvc.perform( - MockMvcRequestBuilders.multipart("/products/1") - .file(imageFile) - .file(productReqDtoFile) - .param("userId", "1") + builder.file(imageFile).file(productReqDtoFile).param("userId", "1") .contentType(MediaType.MULTIPART_FORM_DATA) - ).andExpect(status().isOk) - .andExpect(jsonPath("$.data.title").value("수정된 제목")) + ).andExpect(status().isOk).andExpect(jsonPath("$.data.title").value("수정된 제목")) .andExpect(jsonPath("$.data.writerId").value(1)) } + + private fun loadImageFile(name: String): MockMultipartFile { + val imagePath = Paths.get("src/test/resources/${name}") + return MockMultipartFile( + "images", name, MediaType.IMAGE_PNG_VALUE, Files.readAllBytes(imagePath) + ) + } } \ No newline at end of file diff --git a/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt b/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt index 7a9e6e4..40874a2 100644 --- a/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt +++ b/src/test/kotlin/com/get_offer/product/service/ProductServiceTest.kt @@ -95,14 +95,7 @@ class ProductServiceTest { fun postProductWithValidData() { // given val userId = 1L - val productReqDto = ProductPostReqDto( - title = "Test Product", - description = "Test Description", - startPrice = 1000, - startDate = LocalDateTime.now().plusDays(1), - endDate = LocalDateTime.now().plusDays(3), - category = Category.BOOKS - ) + val productReqDto = makeProductPostDto() val mockImage = MockMultipartFile("images", "test.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) @@ -136,13 +129,8 @@ class ProductServiceTest { fun postProductWithInvalidStartPrice() { // given val userId = 1L - val productReqDto = ProductPostReqDto( - title = "Test Product", - description = "Test Description", + val productReqDto = makeProductPostDto().copy( startPrice = -1000, // Invalid start price - startDate = LocalDateTime.now().plusDays(1), - endDate = LocalDateTime.now().plusDays(3), - category = Category.BOOKS ) val mockImage = MockMultipartFile("images", "test.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) @@ -159,13 +147,9 @@ class ProductServiceTest { fun `test postProduct with invalid date range`() { // given val userId = 1L - val productReqDto = ProductPostReqDto( - title = "Test Product", - description = "Test Description", - startPrice = 1000, - startDate = LocalDateTime.now().plusDays(10), // Invalid start date (after end date) - endDate = LocalDateTime.now().plusDays(3), - category = Category.BOOKS + val productReqDto = makeProductPostDto().copy( + // Invalid start date (after end date)) + startDate = LocalDateTime.now().plusDays(10) ) val mockImage = MockMultipartFile("images", "test.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) @@ -207,4 +191,15 @@ class ProductServiceTest { assertEquals(req.title, result.title) assertEquals(req.writerId, result.writerId) } + + private fun makeProductPostDto(): ProductPostReqDto { + return ProductPostReqDto( + title = "Test Product", + description = "Test Description", + startPrice = 1000, + startDate = LocalDateTime.now().plusDays(10), // Invalid start date (after end date) + endDate = LocalDateTime.now().plusDays(3), + category = Category.BOOKS + ) + } } \ No newline at end of file From 0c8fbeea5b516cbfdafb504ad766759aeecaa646 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Mon, 28 Oct 2024 22:49:04 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat=20:=20S3=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +- .../com/get_offer/common/s3/S3Config.kt | 20 ++++----- .../get_offer/common/s3/S3FileManagement.kt | 44 ++++++++----------- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/build.gradle b/build.gradle index 855c650..2f02d84 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' implementation 'org.jetbrains.kotlin:kotlin-reflect' - implementation("com.amazonaws:aws-java-sdk-s3:1.12.773") + implementation platform('software.amazon.awssdk:bom:2.17.230') + implementation 'software.amazon.awssdk:s3' + implementation "org.springframework.boot:spring-boot-starter-oauth2-client" compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/kotlin/com/get_offer/common/s3/S3Config.kt b/src/main/kotlin/com/get_offer/common/s3/S3Config.kt index b28d6b0..4f8a534 100644 --- a/src/main/kotlin/com/get_offer/common/s3/S3Config.kt +++ b/src/main/kotlin/com/get_offer/common/s3/S3Config.kt @@ -1,13 +1,13 @@ package com.get_offer.common.s3 -import com.amazonaws.auth.AWSStaticCredentialsProvider -import com.amazonaws.auth.BasicAWSCredentials -import com.amazonaws.regions.Regions -import com.amazonaws.services.s3.AmazonS3 -import com.amazonaws.services.s3.AmazonS3ClientBuilder + import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client @Configuration class S3Config( @@ -17,12 +17,10 @@ class S3Config( private val secretKey: String, ) { @Bean - fun amazonS3Client(): AmazonS3 { - val credentials = BasicAWSCredentials(accessKey, secretKey) - return AmazonS3ClientBuilder - .standard() - .withCredentials(AWSStaticCredentialsProvider(credentials)) - .withRegion(Regions.AP_NORTHEAST_2) + fun amazonS3Client(): S3Client { + val credentials = AwsBasicCredentials.create(accessKey, secretKey) + return S3Client.builder().region(Region.AP_NORTHEAST_2) + .credentialsProvider(StaticCredentialsProvider.create(credentials)) .build() } } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt b/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt index 160de2f..56ad790 100644 --- a/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt +++ b/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt @@ -1,18 +1,20 @@ package com.get_offer.common.s3 -import com.amazonaws.services.s3.AmazonS3 -import com.amazonaws.services.s3.model.ObjectMetadata import com.get_offer.multipart.FileValidate import java.util.* import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest +import software.amazon.awssdk.services.s3.model.PutObjectRequest @Component class S3FileManagement( @Value("\${aws.s3.bucket}") private val bucket: String, - private val amazonS3: AmazonS3, + private val amazonS3: S3Client, ) { companion object { const val TYPE_IMAGE = "image" @@ -27,17 +29,16 @@ class S3FileManagement( ?: throw IllegalStateException() FileValidate.checkImageFormat(originalFilename) val fileName = "${UUID.randomUUID()}-${originalFilename}" - val objectMetadata = setFileDateOption( - type = TYPE_IMAGE, - file = getFileExtension(originalFilename), - multipartFile = multipartFile - ) - amazonS3.putObject(bucket, fileName, multipartFile.inputStream, objectMetadata) - return fileName - } - fun getFile(fileName: String): String { - return amazonS3.getUrl(bucket, fileName).toString() + val putObjectRequest = PutObjectRequest.builder() + .bucket(bucket) + .key(fileName) + .contentType("/${TYPE_IMAGE}/${getFileExtension(getFileExtension(originalFilename))}") + .contentLength(multipartFile.inputStream.available().toLong()) + .build() + + amazonS3.putObject(putObjectRequest, RequestBody.fromBytes(multipartFile.bytes)) + return fileName } fun delete(fileNames: List) { @@ -45,22 +46,15 @@ class S3FileManagement( } fun delete(fileName: String) { - amazonS3.deleteObject(bucket, fileName) + val deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(bucket) + .key(fileName) + .build() + amazonS3.deleteObject(deleteObjectRequest) } private fun getFileExtension(fileName: String): String { val extensionIndex = fileName.lastIndexOf('.') return fileName.substring(extensionIndex + 1) } - - private fun setFileDateOption( - type: String, - file: String, - multipartFile: MultipartFile - ): ObjectMetadata { - val objectMetadata = ObjectMetadata() - objectMetadata.contentType = "/${type}/${getFileExtension(file)}" - objectMetadata.contentLength = multipartFile.inputStream.available().toLong() - return objectMetadata - } } \ No newline at end of file From 868bc469cffd1452628f1eb13d4166c6b4579579 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Mon, 28 Oct 2024 23:07:32 +0900 Subject: [PATCH 12/14] =?UTF-8?q?feat=20:=20oauth2=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B6=8C=ED=95=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/oauth/OAuth2LoginConfig.kt | 49 +++++++++++++++++++ .../get_offer/user/controller/LoginRequest.kt | 6 +++ .../user/controller/UserController.kt | 11 +++-- src/main/resources/static/index.html | 11 +++++ 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/com/get_offer/common/oauth/OAuth2LoginConfig.kt create mode 100644 src/main/kotlin/com/get_offer/user/controller/LoginRequest.kt create mode 100644 src/main/resources/static/index.html diff --git a/src/main/kotlin/com/get_offer/common/oauth/OAuth2LoginConfig.kt b/src/main/kotlin/com/get_offer/common/oauth/OAuth2LoginConfig.kt new file mode 100644 index 0000000..7f7be72 --- /dev/null +++ b/src/main/kotlin/com/get_offer/common/oauth/OAuth2LoginConfig.kt @@ -0,0 +1,49 @@ +package com.get_offer.common.oauth + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.oauth2.client.registration.ClientRegistration +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository +import org.springframework.security.oauth2.core.AuthorizationGrantType +import org.springframework.security.oauth2.core.ClientAuthenticationMethod +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames +import org.springframework.security.web.SecurityFilterChain + + +@Configuration +class OAuth2LoginConfig( + @Value("\${spring.security.oauth2.client.registration.google-login.client-id}") + private val clientId: String, + @Value("\${spring.security.oauth2.client.registration.google-login.client-secret}") + private val clientSecret: String, +) { + @Bean + fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + val baseUrl = "http://localhost:8080" + val registrationId = "google" + return ClientRegistration.withRegistrationId("google").clientId(clientId).clientSecret(clientSecret) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("${baseUrl}/login/oauth2/code/${registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo").userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs").clientName("Google").build() + } + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + http.authorizeHttpRequests { authorizeHttpRequests -> + authorizeHttpRequests.requestMatchers("/**").hasRole("USER") + }.oauth2Login { } + return http.build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/user/controller/LoginRequest.kt b/src/main/kotlin/com/get_offer/user/controller/LoginRequest.kt new file mode 100644 index 0000000..a36acc8 --- /dev/null +++ b/src/main/kotlin/com/get_offer/user/controller/LoginRequest.kt @@ -0,0 +1,6 @@ +package com.get_offer.user.controller + +data class LoginRequest( + val username: String, + val password: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/user/controller/UserController.kt b/src/main/kotlin/com/get_offer/user/controller/UserController.kt index 59ca783..beddda4 100644 --- a/src/main/kotlin/com/get_offer/user/controller/UserController.kt +++ b/src/main/kotlin/com/get_offer/user/controller/UserController.kt @@ -4,17 +4,22 @@ import ApiResponse import com.get_offer.user.service.UserInfoDto import com.get_offer.user.service.UserService import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/users") class UserController( private val userService: UserService, ) { - @GetMapping() + @GetMapping("/users") fun getUserInfo(@RequestParam userId: String): ApiResponse { return ApiResponse.success(userService.getUserInfo(userId.toLong())) } + + @PostMapping("/login/oauth2/code/google") + fun login(@RequestBody loginRequest: LoginRequest) { + println("hi") + } } \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..c7ef95e --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,11 @@ + + + + + OAuth 2.0 로그인 + + +Google Login
+ + + \ No newline at end of file From f62274141b3a30440722894cfbbd5c9549fdfb00 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Thu, 31 Oct 2024 22:10:57 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat=20:=20oauth2=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=86=A0=EA=B7=BC=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../common/oauth/OAuth2LoginConfig.kt | 20 +++- .../com/get_offer/user/LoginController.kt | 108 ++++++++++++++++++ .../get_offer/user/controller/LoginRequest.kt | 6 - .../get_offer/user/controller/TokenRequest.kt | 6 + .../user/controller/UserController.kt | 8 +- src/main/resources/static/index.html | 11 -- .../resources/templates/loginSuccess.html | 9 ++ src/main/resources/templates/oauth_login.html | 4 + 9 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/com/get_offer/user/LoginController.kt delete mode 100644 src/main/kotlin/com/get_offer/user/controller/LoginRequest.kt create mode 100644 src/main/kotlin/com/get_offer/user/controller/TokenRequest.kt delete mode 100644 src/main/resources/static/index.html create mode 100644 src/main/resources/templates/loginSuccess.html create mode 100644 src/main/resources/templates/oauth_login.html diff --git a/build.gradle b/build.gradle index 2f02d84..368cc59 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.jetbrains.kotlin:kotlin-reflect' implementation platform('software.amazon.awssdk:bom:2.17.230') implementation 'software.amazon.awssdk:s3' diff --git a/src/main/kotlin/com/get_offer/common/oauth/OAuth2LoginConfig.kt b/src/main/kotlin/com/get_offer/common/oauth/OAuth2LoginConfig.kt index 7f7be72..846cbbc 100644 --- a/src/main/kotlin/com/get_offer/common/oauth/OAuth2LoginConfig.kt +++ b/src/main/kotlin/com/get_offer/common/oauth/OAuth2LoginConfig.kt @@ -4,6 +4,9 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest import org.springframework.security.oauth2.client.registration.ClientRegistration import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository @@ -42,8 +45,21 @@ class OAuth2LoginConfig( @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { http.authorizeHttpRequests { authorizeHttpRequests -> - authorizeHttpRequests.requestMatchers("/**").hasRole("USER") - }.oauth2Login { } + authorizeHttpRequests.requestMatchers("/oauth_login", "/loginSuccess").permitAll() + .anyRequest() + .authenticated() + }.oauth2Login { it -> + it.loginPage("/oauth_login") + it.defaultSuccessUrl("/loginSuccess", true) + it.tokenEndpoint { + it.accessTokenResponseClient(accessTokenResponseClient()) + } + } return http.build() } + + @Bean + fun accessTokenResponseClient(): OAuth2AccessTokenResponseClient { + return DefaultAuthorizationCodeTokenResponseClient() + } } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/user/LoginController.kt b/src/main/kotlin/com/get_offer/user/LoginController.kt new file mode 100644 index 0000000..f3ecd42 --- /dev/null +++ b/src/main/kotlin/com/get_offer/user/LoginController.kt @@ -0,0 +1,108 @@ +package com.get_offer.user + +import com.get_offer.user.controller.TokenRequest +import org.springframework.core.ResolvableType +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest +import org.springframework.security.oauth2.client.registration.ClientRegistration +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.client.RestTemplate + + +@Controller +class LoginController( + private val clientRegistrationRepository: ClientRegistrationRepository, + private val accessTokenResponseClient: OAuth2AccessTokenResponseClient, + private val authorizedClientService: OAuth2AuthorizedClientService, +) { + + @GetMapping("/oauth_login") + fun getLoginPage(model: Model): String { + val authorizationRequestBaseUri = "oauth2/authorization" + val oauth2AuthenticationUrls: MutableMap = HashMap() + var clientRegistrations: Iterable? = null + val type = ResolvableType.forInstance(clientRegistrationRepository) + .`as`(Iterable::class.java) + + if (type != ResolvableType.NONE && + ClientRegistration::class.java.isAssignableFrom(type.resolveGenerics()[0]) + ) { + clientRegistrations = clientRegistrationRepository as Iterable + } + + clientRegistrations?.forEach { registration -> + oauth2AuthenticationUrls[registration.clientName] = + "$authorizationRequestBaseUri/${registration.registrationId}" + } + + model.addAttribute("urls", oauth2AuthenticationUrls) + + return "oauth_login" + } + + @GetMapping("/loginSuccess") + fun loadUser(model: Model, authentication: OAuth2AuthenticationToken): String { + val client = authorizedClientService.loadAuthorizedClient( + authentication.authorizedClientRegistrationId, authentication.name + ) + + val userInfoEndpointUri = client.clientRegistration + .providerDetails.userInfoEndpoint.uri + + if (!userInfoEndpointUri.isNullOrEmpty()) { + val restTemplate = RestTemplate() + val headers = HttpHeaders().apply { + add(HttpHeaders.AUTHORIZATION, "Bearer ${client.accessToken.tokenValue}") + } + println(client.accessToken.tokenValue) // 후에 수정 필요 + val entity = HttpEntity("", headers) + val response = restTemplate.exchange( + userInfoEndpointUri, + HttpMethod.GET, + entity, + Map::class.java + ) + val userAttributes = response.body ?: emptyMap() + model.addAttribute("name", userAttributes["name"]) + } + return "loginSuccess" + } + + @PostMapping("/token") + fun getAccessToken(@RequestBody req: TokenRequest): String? { + val clientRegistration = clientRegistrationRepository.findByRegistrationId("google") + + val authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .clientId(clientRegistration.clientId) + .redirectUri(req.redirectUri) + .authorizationUri(clientRegistration.providerDetails.authorizationUri) + .build() + + val authorizationResponse = OAuth2AuthorizationResponse.success(req.authorizationCode) + .redirectUri(req.redirectUri) + .build() + + val authorizationExchange = OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse) + + // Access Token 발급 요청 + val tokenRequest = OAuth2AuthorizationCodeGrantRequest(clientRegistration, authorizationExchange) + val tokenResponse = accessTokenResponseClient.getTokenResponse(tokenRequest) + + // Access Token 반환 + return tokenResponse.accessToken.tokenValue + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/user/controller/LoginRequest.kt b/src/main/kotlin/com/get_offer/user/controller/LoginRequest.kt deleted file mode 100644 index a36acc8..0000000 --- a/src/main/kotlin/com/get_offer/user/controller/LoginRequest.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.get_offer.user.controller - -data class LoginRequest( - val username: String, - val password: String -) \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/user/controller/TokenRequest.kt b/src/main/kotlin/com/get_offer/user/controller/TokenRequest.kt new file mode 100644 index 0000000..152ba80 --- /dev/null +++ b/src/main/kotlin/com/get_offer/user/controller/TokenRequest.kt @@ -0,0 +1,6 @@ +package com.get_offer.user.controller + +data class TokenRequest( + val redirectUri: String, + val authorizationCode: String, +) \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/user/controller/UserController.kt b/src/main/kotlin/com/get_offer/user/controller/UserController.kt index beddda4..a2add8d 100644 --- a/src/main/kotlin/com/get_offer/user/controller/UserController.kt +++ b/src/main/kotlin/com/get_offer/user/controller/UserController.kt @@ -4,11 +4,10 @@ import ApiResponse import com.get_offer.user.service.UserInfoDto import com.get_offer.user.service.UserService import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController + @RestController class UserController( private val userService: UserService, @@ -17,9 +16,4 @@ class UserController( fun getUserInfo(@RequestParam userId: String): ApiResponse { return ApiResponse.success(userService.getUserInfo(userId.toLong())) } - - @PostMapping("/login/oauth2/code/google") - fun login(@RequestBody loginRequest: LoginRequest) { - println("hi") - } } \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html deleted file mode 100644 index c7ef95e..0000000 --- a/src/main/resources/static/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - OAuth 2.0 로그인 - - -Google Login
- - - \ No newline at end of file diff --git a/src/main/resources/templates/loginSuccess.html b/src/main/resources/templates/loginSuccess.html new file mode 100644 index 0000000..d56c9af --- /dev/null +++ b/src/main/resources/templates/loginSuccess.html @@ -0,0 +1,9 @@ + + Login Success + + +

Login Success

+

+ Welcome, User! +

+ \ No newline at end of file diff --git a/src/main/resources/templates/oauth_login.html b/src/main/resources/templates/oauth_login.html new file mode 100644 index 0000000..4977da1 --- /dev/null +++ b/src/main/resources/templates/oauth_login.html @@ -0,0 +1,4 @@ +

Login with:

+

+ Client +

\ No newline at end of file From 7f75d881d5d5ab40f5c33967258c6b3d9b5a8fa5 Mon Sep 17 00:00:00 2001 From: pine_lee Date: Thu, 31 Oct 2024 22:20:34 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feat=20:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../get_offer/common/s3/S3FileManagement.kt | 2 +- .../com/get_offer/multipart/FileValidate.kt | 2 +- .../com/get_offer/product/domain/Product.kt | 61 +++++++++---------- .../product/service/ProductService.kt | 2 +- 4 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt b/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt index 56ad790..c4f57fd 100644 --- a/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt +++ b/src/main/kotlin/com/get_offer/common/s3/S3FileManagement.kt @@ -45,7 +45,7 @@ class S3FileManagement( fileNames.forEach { delete(it) } } - fun delete(fileName: String) { + private fun delete(fileName: String) { val deleteObjectRequest = DeleteObjectRequest.builder() .bucket(bucket) .key(fileName) diff --git a/src/main/kotlin/com/get_offer/multipart/FileValidate.kt b/src/main/kotlin/com/get_offer/multipart/FileValidate.kt index dc37f33..861c29a 100644 --- a/src/main/kotlin/com/get_offer/multipart/FileValidate.kt +++ b/src/main/kotlin/com/get_offer/multipart/FileValidate.kt @@ -4,7 +4,7 @@ import com.get_offer.common.exception.UnsupportedFileExtensionException class FileValidate { companion object { - private val IMAGE_EXTENSIONS: List = listOf("jpg", "png", "jpeg") + private val IMAGE_EXTENSIONS: List = listOf("jpg", "png", "jpeg", "JPG", "PNG", "JPEG") fun checkImageFormat(fileName: String) { val extensionIndex = fileName.lastIndexOf('.') diff --git a/src/main/kotlin/com/get_offer/product/domain/Product.kt b/src/main/kotlin/com/get_offer/product/domain/Product.kt index 489d2ca..188060b 100644 --- a/src/main/kotlin/com/get_offer/product/domain/Product.kt +++ b/src/main/kotlin/com/get_offer/product/domain/Product.kt @@ -24,7 +24,9 @@ class Product( @Enumerated(EnumType.STRING) val category: Category, - @Convert(converter = ProductImagesConverter::class) @Column(name = "IMAGES") val images: ProductImagesVo, + @Convert(converter = ProductImagesConverter::class) + @Column(name = "IMAGES") + val images: ProductImagesVo, val description: String, @@ -32,15 +34,22 @@ class Product( var currentPrice: Int, - @Enumerated(EnumType.STRING) var status: ProductStatus, + @Enumerated(EnumType.STRING) + var status: ProductStatus, var startDate: LocalDateTime, var endDate: LocalDateTime, - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = 0L, + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long = 0L, ) : AuditingTimeEntity() { - fun updateProduct(dto: ProductEditReq): Product { + init { + validateProduct(startPrice, startDate, endDate) + } + + fun updateProduct(dto: ProductEditReq) { if (dto.startPrice != null) { validateStartPrice(dto.startPrice) } @@ -48,44 +57,30 @@ class Product( val startDate = dto.startDate ?: this.startDate val endDate = dto.endDate ?: this.endDate validateDateRange(startDate, endDate) + } - return Product( - id = this.id, - title = dto.title ?: this.title, - description = dto.description ?: this.description, - startPrice = dto.startPrice ?: this.startPrice, - currentPrice = dto.startPrice ?: this.startPrice, - category = dto.category ?: this.category, - status = ProductStatus.WAIT, - startDate = startDate, - endDate = endDate, - images = dto.images?.let { ProductImagesVo(it) } ?: this.images, - writerId = this.writerId, - ) + private fun validateProduct(startPrice: Int, startDate: LocalDateTime, endDate: LocalDateTime) { + validateStartPrice(startPrice) + validateDateRange(startDate, endDate) } - companion object { - fun validateProduct(startPrice: Int, startDate: LocalDateTime, endDate: LocalDateTime) { - validateStartPrice(startPrice) - validateDateRange(startDate, endDate) + private fun validateStartPrice(startPrice: Int) { + if (startPrice < 0) { + throw BadRequestException("startPrice가 0보다 작을 수 없습니다.") } + } + private fun validateDateRange(startDate: LocalDateTime, endDate: LocalDateTime) { + if (startDate.isAfter(endDate)) throw BadRequestException("시작 날짜가 유효하지 않습니다.") + if (ChronoUnit.DAYS.between(startDate, endDate) > 7) throw BadRequestException("경매 기간은 7일을 넘길 수 없습니다.") + } + + companion object { fun checkStatus(startDate: LocalDateTime): ProductStatus { if (LocalDateTime.now().isBefore(startDate)) { return ProductStatus.WAIT } return ProductStatus.IN_PROGRESS } - - private fun validateStartPrice(startPrice: Int) { - if (startPrice < 0) { - throw BadRequestException("startPrice가 0보다 작을 수 없습니다.") - } - } - - private fun validateDateRange(startDate: LocalDateTime, endDate: LocalDateTime) { - if (startDate.isAfter(endDate)) throw BadRequestException("시작 날짜가 유효하지 않습니다.") - if (ChronoUnit.DAYS.between(startDate, endDate) > 7) throw BadRequestException("경매 기간은 7일을 넘길 수 없습니다.") - } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/get_offer/product/service/ProductService.kt b/src/main/kotlin/com/get_offer/product/service/ProductService.kt index a02239f..7c8ea05 100644 --- a/src/main/kotlin/com/get_offer/product/service/ProductService.kt +++ b/src/main/kotlin/com/get_offer/product/service/ProductService.kt @@ -82,7 +82,7 @@ class ProductService( } else null - product = product.updateProduct(ProductEditReq.of(req, imageUrls)) + product.updateProduct(ProductEditReq.of(req, imageUrls)) productRepository.save(product) return ProductSaveDto.of(product)