diff --git a/build.gradle.kts b/build.gradle.kts index 69dcece..3ca9363 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -82,7 +82,10 @@ dependencies { annotationProcessor("org.projectlombok:lombok") testAnnotationProcessor("org.projectlombok:lombok") testImplementation("org.springframework.boot:spring-boot-starter-test") - implementation("org.springframework.boot:spring-boot-configuration-processor") + + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") + implementation("org.springframework.cloud:spring-cloud-aws-context:2.2.6.RELEASE") + implementation("com.querydsl:querydsl-jpa:${queryDslVersion}") annotationProcessor("com.querydsl:querydsl-apt:${queryDslVersion}") diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/auctionItem/dto/AuctionItemResponse.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/auctionItem/dto/AuctionItemResponse.java index 75dbbd9..6db1e15 100644 --- a/src/main/java/megabrain/gyeongnamgyeongmae/domain/auctionItem/dto/AuctionItemResponse.java +++ b/src/main/java/megabrain/gyeongnamgyeongmae/domain/auctionItem/dto/AuctionItemResponse.java @@ -1,10 +1,9 @@ package megabrain.gyeongnamgyeongmae.domain.auctionItem.dto; import java.time.LocalDateTime; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import java.util.List; + +import lombok.*; import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.entity.AuctionItem; import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.entity.AuctionItemStatus; import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.entity.AuctionStatus; @@ -14,6 +13,7 @@ @Getter @NoArgsConstructor @AllArgsConstructor +@Setter public class AuctionItemResponse { private Long id; @@ -37,6 +37,8 @@ public class AuctionItemResponse { private Long likeCount; private Long viewCount; + private List images; + public static AuctionItemResponse of(AuctionItem auctionItem) { return AuctionItemResponse.builder() .id(auctionItem.getId()) diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/auctionItem/service/Item/AuctionItemServiceImpl.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/auctionItem/service/Item/AuctionItemServiceImpl.java index 5defd2d..d09ef3c 100644 --- a/src/main/java/megabrain/gyeongnamgyeongmae/domain/auctionItem/service/Item/AuctionItemServiceImpl.java +++ b/src/main/java/megabrain/gyeongnamgyeongmae/domain/auctionItem/service/Item/AuctionItemServiceImpl.java @@ -1,7 +1,10 @@ package megabrain.gyeongnamgyeongmae.domain.auctionItem.service.Item; import java.time.LocalDateTime; +import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; + import lombok.RequiredArgsConstructor; import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.entity.AuctionItem; import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.entity.AuctionItemLike; @@ -15,6 +18,8 @@ import megabrain.gyeongnamgyeongmae.domain.category.domain.entity.Category; import megabrain.gyeongnamgyeongmae.domain.category.domain.repository.CategoryRepository; import megabrain.gyeongnamgyeongmae.domain.category.service.CategoryService; +import megabrain.gyeongnamgyeongmae.domain.image.domain.entity.Image; +import megabrain.gyeongnamgyeongmae.domain.image.domain.repository.ImageRepository; import megabrain.gyeongnamgyeongmae.domain.user.domain.entity.User; import megabrain.gyeongnamgyeongmae.domain.user.domain.repository.UserRepository; import megabrain.gyeongnamgyeongmae.domain.user.service.UserService; @@ -31,6 +36,7 @@ public class AuctionItemServiceImpl implements AuctionItemService { private final UserRepository userRepository; private final CategoryRepository categoryRepository; private final UserService userService; + private final ImageRepository imageRepository; @Override @Transactional @@ -52,9 +58,19 @@ public void createAuctionItem(CreateAuctionItemRequest createAuctionItemRequest) @Transactional public AuctionItemResponse findAuctionItemById(Long id) { AuctionItem auctionItem = auctionItemRepository.findById(id).orElseThrow(RuntimeException::new); + List images = imageRepository.findImageByAuctionItemId(id); auctionItem.checkShowAuctionItem(auctionItem); updateAuctionItemViewCount(auctionItem); - return AuctionItemResponse.of(auctionItem); + AuctionItemResponse auctionItemResponse = AuctionItemResponse.of(auctionItem); + if (!images.isEmpty()){ + List imageUrls = images.stream().map(this::makeImageUrl).collect(Collectors.toList()); + auctionItemResponse.setImages(imageUrls); + } + return auctionItemResponse; + } + + private String makeImageUrl(Image image){ + return image.getImageUrl(); } @Override diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/domain/Image.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/domain/Image.java deleted file mode 100644 index 6f6178b..0000000 --- a/src/main/java/megabrain/gyeongnamgyeongmae/domain/domain/Image.java +++ /dev/null @@ -1,39 +0,0 @@ -package megabrain.gyeongnamgyeongmae.domain.domain; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.entity.AuctionItem; -import megabrain.gyeongnamgyeongmae.global.BaseTimeEntity; - -@Entity -@Table(name = "images", indexes = @Index(name = "url", columnList = "image_url")) -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Image extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id") - private Long id; - - @NotNull - @Column(name = "image_url") - private String url; - - @Column(name = "removed") - private boolean removed; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "auction_id") - private AuctionItem auctionItem; - - // @Builder - // public Image(String url, AuctionItem auctionItem) { - // this.url = url; - // this.auctionItem = auctionItem; - // } - -} diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Controller/ImageController.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Controller/ImageController.java new file mode 100644 index 0000000..6031ca1 --- /dev/null +++ b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Controller/ImageController.java @@ -0,0 +1,35 @@ +package megabrain.gyeongnamgyeongmae.domain.image.Controller; + + +import io.swagger.v3.oas.annotations.Operation; + +import lombok.RequiredArgsConstructor; +import megabrain.gyeongnamgyeongmae.domain.image.Service.ImageService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + + +@RequiredArgsConstructor +@RestController +@RequestMapping("api/{from}/{id}/images/") +public class ImageController { + + private final ImageService imageService; + + @PostMapping(value = "/upload", consumes = { + MediaType.MULTIPART_FORM_DATA_VALUE + }) + @Operation(summary = "이미지", description = "이미지 업로드") + public ResponseEntity uploadImage( + @RequestPart("file") List files, @PathVariable String from, @PathVariable Long id) throws IOException { + imageService.uploadImage(files, from, id); + + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Service/AwsS3Service.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Service/AwsS3Service.java new file mode 100644 index 0000000..786c8bc --- /dev/null +++ b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Service/AwsS3Service.java @@ -0,0 +1,45 @@ +package megabrain.gyeongnamgyeongmae.domain.image.Service; + +import com.amazonaws.SdkClientException; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + + +@Service +public class AwsS3Service { + + private final AmazonS3 amazonS3; + + @Autowired + public AwsS3Service(AmazonS3 amazonS3) { + this.amazonS3 = amazonS3; + } + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public String upload(MultipartFile file, String filename) throws IOException { + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(file.getContentType()); + objectMetadata.setContentLength(file.getSize()); + + try { + amazonS3.putObject(new PutObjectRequest(bucket, filename, file.getInputStream(), objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch (SdkClientException e) { + e.printStackTrace(); + System.out.println("업로드 실패: " + e.getMessage()); + } + + return amazonS3.getUrl(bucket, filename).toString(); + } +} + diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Service/ImageService.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Service/ImageService.java new file mode 100644 index 0000000..785cb65 --- /dev/null +++ b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Service/ImageService.java @@ -0,0 +1,13 @@ +package megabrain.gyeongnamgyeongmae.domain.image.Service; + + +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +public interface ImageService { + + void uploadImage(List images, String from, Long id) throws IOException; + +} diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Service/ImageServiceImpl.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Service/ImageServiceImpl.java new file mode 100644 index 0000000..28bb691 --- /dev/null +++ b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/Service/ImageServiceImpl.java @@ -0,0 +1,100 @@ +package megabrain.gyeongnamgyeongmae.domain.image.Service; + +import lombok.RequiredArgsConstructor; +import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.entity.AuctionItem; +import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.entity.Comment; +import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.repostiory.AuctionItemCommentRepository; +import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.repostiory.AuctionItemRepository; +import megabrain.gyeongnamgyeongmae.domain.image.domain.repository.ImageRepository; +import megabrain.gyeongnamgyeongmae.domain.image.dto.FileType; +import megabrain.gyeongnamgyeongmae.domain.image.domain.entity.Image; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class ImageServiceImpl implements ImageService { + + private final ImageRepository imageRepository; + private final AwsS3Service awsS3Service; + private final AuctionItemRepository auctionItemRepository; + private final AuctionItemCommentRepository commentRepository; + + @Override + public void uploadImage(List images, String from, Long id) throws IOException { + String whereFrom = checkImageUploadFind(from); + AuctionItem auctionItem = null; + Comment comment = null; + if (whereFrom.equals("AuctionItem")) { + auctionItem = checkIsRealIdAuctionItem(id); + } + if (whereFrom.equals("Comment")) { + comment = checkIsRealIdComment(id); + } + upload(images, whereFrom, auctionItem, comment); + } + + private AuctionItem checkIsRealIdAuctionItem(Long id) { + return auctionItemRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않는 경매")); + } + + private Comment checkIsRealIdComment(Long id) { + return commentRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않는 댓글")); + } + + private void upload(List images, String from, AuctionItem auctionItem, Comment comment) throws IOException { + for (MultipartFile file : images) { + String originalFilename = file.getOriginalFilename(); + + if (originalFilename == null) { + throw new RuntimeException("파일 확장자 없음"); + } + + String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); + + if (!FileType.isValid(fileExtension)) { + throw new RuntimeException("이미지 파일 아님"); + } + + String fileName = uploadFileName(from, fileExtension); + awsS3Service.upload(file, fileName); + + String fileShow = "https://d231cnlxdxmjew.cloudfront.net/" + fileName; + Image image = Image.builder() + .imageFrom(from) + .name(originalFilename) + .url(fileShow) + .build(); + image.setAuctionItem(auctionItem); + image.setComment(comment); + imageRepository.save(image); + } + } + + private String uploadFileName(String from, String fileExtension) { + return from + "/" + (UUID.randomUUID().toString().replace("-", "") + "." + fileExtension); + + } + + private String checkImageUploadFind(String from) { + if (from.equals("Profile")) { + return "Profile"; + } + if (from.equals("AuctionItem")) { + return "AuctionItem"; + } + if (from.equals("Comment")) { + return "Comment"; + } + throw new RuntimeException("이미지 파라미터 잘못"); + } + +} + + + + diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/domain/entity/Image.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/domain/entity/Image.java new file mode 100644 index 0000000..3d22ce5 --- /dev/null +++ b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/domain/entity/Image.java @@ -0,0 +1,45 @@ +package megabrain.gyeongnamgyeongmae.domain.image.domain.entity; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.entity.AuctionItem; +import megabrain.gyeongnamgyeongmae.domain.auctionItem.domain.entity.Comment; +import javax.persistence.*; + +@NoArgsConstructor +@Entity +@Table(name = "Image") +@Getter +@Setter +public class Image { + + @Id + @Column(name = "image_url") + private String imageUrl; + + @Column(name = "image_name") + private String imageName; + + @Column(name = "image_from") + private String imageFrom; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "auction_id") + private AuctionItem auctionItem; + + @Column(name = "removed") + private boolean removed; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name ="comment_id") + private Comment comment; + + @Builder + public Image(String name, String url, String imageFrom) { + this.imageName = name; + this.imageUrl = url; + this.imageFrom = imageFrom; + } +} diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/domain/repository/ImageRepository.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/domain/repository/ImageRepository.java new file mode 100644 index 0000000..2d8dfa3 --- /dev/null +++ b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/domain/repository/ImageRepository.java @@ -0,0 +1,14 @@ +package megabrain.gyeongnamgyeongmae.domain.image.domain.repository; + +import megabrain.gyeongnamgyeongmae.domain.image.domain.entity.Image; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface ImageRepository extends JpaRepository { + + @Query(value = "SELECT DISTINCT * FROM Image where auction_id = :id", nativeQuery = true) + List findImageByAuctionItemId(Long id); +} diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/dto/FileType.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/dto/FileType.java new file mode 100644 index 0000000..d60fb88 --- /dev/null +++ b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/dto/FileType.java @@ -0,0 +1,26 @@ +package megabrain.gyeongnamgyeongmae.domain.image.dto; + +public enum FileType { + JPG("jpg"), + JPEG("jpeg"), + PNG("png"); + + private final String extension; + + FileType(String extension) { + this.extension = extension; + } + + public String getExtension() { + return extension; + } + + public static boolean isValid(String extension) { + for (FileType imageExtension : FileType.values()) { + if (imageExtension.getExtension().equalsIgnoreCase(extension)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/dto/ImageType.java b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/dto/ImageType.java new file mode 100644 index 0000000..451e084 --- /dev/null +++ b/src/main/java/megabrain/gyeongnamgyeongmae/domain/image/dto/ImageType.java @@ -0,0 +1,6 @@ +package megabrain.gyeongnamgyeongmae.domain.image.dto; + +public enum ImageType { + PROFILE, + AUCTIONITEM, +} diff --git a/src/main/java/megabrain/gyeongnamgyeongmae/global/config/AwsS3Config.java b/src/main/java/megabrain/gyeongnamgyeongmae/global/config/AwsS3Config.java new file mode 100644 index 0000000..0ecca70 --- /dev/null +++ b/src/main/java/megabrain/gyeongnamgyeongmae/global/config/AwsS3Config.java @@ -0,0 +1,35 @@ +package megabrain.gyeongnamgyeongmae.global.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; + +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 +public class AwsS3Config { + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3Client() { + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1c92dee..21d09fb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,7 +1,27 @@ server.port: ${PORT} spring.config.import: - application-mailer.yml - - application-redis.yml - application-postgresql.yml - # oauth2 vendors +# oauth2 vendors - oauth/kakao-oauth.yml +cloud: + aws: + s3: + bucket: ${cloud.aws.s3.bucket} + credentials: + access-key: ${cloud.aws.credentials.access-key} + secret-key: ${cloud.aws.credentials.secret-key} + region: + static: ${cloud.aws.region.static} + auto: ${cloud.aws.region.auto} +# region: +# static: ${cloud.aws.region.static} +# auto: ${cloud.aws.region.auto} + stack: + auto: false + +spring: + servlet: + multipart: + max-file-size: 20MB + max-request-size: 20MB \ No newline at end of file