Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

이미지 관리 테이블 작성한다 #67

Open
wants to merge 19 commits into
base: week7
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/main/java/poomasi/domain/image/controller/ImageController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package poomasi.domain.image.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import poomasi.domain.image.dto.ImageRequest;
import poomasi.domain.image.entity.Image;
import poomasi.domain.image.entity.ImageType;
import poomasi.domain.image.service.ImageService;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/image")
public class ImageController {
private final ImageService imageService;

// 이미지 정보 저장
@PostMapping
public ResponseEntity<?> saveImageInfo(@RequestBody ImageRequest imageRequest) {
Image savedImage = imageService.saveImage(imageRequest);
return ResponseEntity.ok(savedImage);
}

// 여러 이미지 정보 저장
@PostMapping("/multiple")
public ResponseEntity<List<Image>> saveMultipleImages(@RequestBody List<ImageRequest> imageRequests) {
List<Image> savedImages = imageService.saveMultipleImages(imageRequests);
return ResponseEntity.ok(savedImages);
}

// 특정 이미지 삭제
@DeleteMapping("delete/{id}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 /빠진 것 같습니당~~

public ResponseEntity<Void> deleteImage(@PathVariable Long id) {
imageService.deleteImage(id);
return ResponseEntity.noContent().build();
}

// 특정 이미지 조회
@GetMapping("/{id}")
public ResponseEntity<Image> getImage(@PathVariable Long id) {
return ResponseEntity.ok(imageService.getImageById(id));
}

// 모든 이미지 조회 (특정 referenceId에 따라)
@GetMapping("/reference/{type}/{referenceId}")
public ResponseEntity<List<Image>> getImagesByTypeAndReference(@PathVariable ImageType type, @PathVariable Long referenceId) {
List<Image> images = imageService.getImagesByTypeAndReferenceId(type, referenceId);
return ResponseEntity.ok(images);
}

// 이미지 정보 수정
@PutMapping("/{id}")
public ResponseEntity<?> updateImageInfo(@PathVariable Long id, @RequestBody ImageRequest imageRequest) {
Image updatedImage = imageService.updateImage(id, imageRequest);
return ResponseEntity.ok(updatedImage);
}


}
16 changes: 16 additions & 0 deletions src/main/java/poomasi/domain/image/dto/ImageRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package poomasi.domain.image.dto;

import poomasi.domain.image.entity.Image;
import poomasi.domain.image.entity.ImageType;

public record ImageRequest(String objectKey, String imageUrl, ImageType type, Long referenceId) {
public Image toEntity(ImageRequest imageRequest){
return new Image(
imageRequest.objectKey,
imageRequest.imageUrl,
imageRequest.type,
imageRequest.referenceId
);
}
}

54 changes: 54 additions & 0 deletions src/main/java/poomasi/domain/image/entity/Image.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package poomasi.domain.image.entity;

import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import poomasi.domain.image.dto.ImageRequest;

import java.util.Date;

@Entity
@Table(name = "images", uniqueConstraints = {
@UniqueConstraint(columnNames = {"type", "reference_id", "object_key"})
})
@Getter
@Setter
@NoArgsConstructor
@SQLDelete(sql = "UPDATE Image SET deleted_at = current_timestamp WHERE id = ?")
public class Image {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String objectKey;

@Column(nullable = false)
private String imageUrl;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ImageType type;

@Column(name = "reference_id", nullable = false)
private Long referenceId;

@Column(name = "created_at", nullable = false, updatable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt = new Date();

public Image(String objectKey, String imageUrl, ImageType type, Long referenceId) {
this.objectKey = objectKey;
this.imageUrl = imageUrl;
this.type = type;
this.referenceId = referenceId;
}

public void update(ImageRequest request) {
this.objectKey = request.objectKey();
this.imageUrl = request.imageUrl();
this.type = request.type();
this.referenceId = request.referenceId();
}
}
5 changes: 5 additions & 0 deletions src/main/java/poomasi/domain/image/entity/ImageType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package poomasi.domain.image.entity;

public enum ImageType {
FARM, FARM_REVIEW, PRODUCT, PRODUCT_REVIEW
}
13 changes: 13 additions & 0 deletions src/main/java/poomasi/domain/image/repository/ImageRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package poomasi.domain.image.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import poomasi.domain.image.entity.Image;
import poomasi.domain.image.entity.ImageType;

import java.util.List;

public interface ImageRepository extends JpaRepository<Image, Long> {
long countByTypeAndReferenceId(ImageType type, Long referenceId);
boolean existsByObjectKeyAndReferenceId(String objectKey, Long referenceId);
List<Image> findByTypeAndReferenceId(ImageType type, Long referenceId);
}
74 changes: 74 additions & 0 deletions src/main/java/poomasi/domain/image/service/ImageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package poomasi.domain.image.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import poomasi.domain.image.dto.ImageRequest;
import poomasi.domain.image.entity.Image;
import poomasi.domain.image.entity.ImageType;
import poomasi.domain.image.repository.ImageRepository;
import poomasi.global.error.BusinessException;

import java.util.List;
import java.util.stream.Collectors;

import static poomasi.global.error.BusinessError.*;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ImageService {
private final ImageRepository imageRepository;

@Transactional
public Image saveImage(ImageRequest imageRequest) {
if (imageRepository.countByTypeAndReferenceId(imageRequest.type(), imageRequest.referenceId()) >= 5) {
throw new BusinessException(IMAGE_LIMIT_EXCEED);
}

if (imageRepository.existsByObjectKeyAndReferenceId(imageRequest.objectKey(), imageRequest.referenceId())) {
throw new BusinessException(IMAGE_ALREADY_EXISTS);
}

Image imageEntity = imageRequest.toEntity(imageRequest);
return imageRepository.save(imageEntity);
}

// 여러 이미지 저장
@Transactional
public List<Image> saveMultipleImages(List<ImageRequest> imageRequests) {
return imageRequests.stream()
.map(this::saveImage)
.collect(Collectors.toList());
}

@Transactional
public void deleteImage(Long id) {
imageRepository.deleteById(id);
}

public Image getImageById(Long id) {
return imageRepository.findById(id)
.orElseThrow(() -> new BusinessException(IMAGE_NOT_FOUND));
}

public List<Image> getImagesByTypeAndReferenceId(ImageType type, Long referenceId) {
return imageRepository.findByTypeAndReferenceId(type, referenceId);
}

// 이미지 수정
@Transactional
public Image updateImage(Long id, ImageRequest imageRequest) {
Image image = imageRepository.findById(id)
.orElseThrow(() -> new BusinessException(IMAGE_NOT_FOUND));

if (imageRepository.countByTypeAndReferenceId(imageRequest.type(), imageRequest.referenceId()) >= 5 &&
!image.getType().equals(imageRequest.type())) {
throw new BusinessException(IMAGE_LIMIT_EXCEED);
}

image.update(imageRequest);

return imageRepository.save(image);
}
}
9 changes: 7 additions & 2 deletions src/main/java/poomasi/global/config/s3/S3Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import poomasi.global.config.aws.AwsProperties;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Client;
Expand All @@ -20,7 +21,11 @@ public class S3Config {

@Bean("awsCredentials")
public AwsCredentialsProvider awsCredentials() {
return DefaultCredentialsProvider.create();
AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(
awsProperties.getAccess(),
awsProperties.getSecret()
);
return StaticCredentialsProvider.create(awsBasicCredentials);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import poomasi.global.config.aws.AwsProperties;
import poomasi.global.config.s3.dto.request.PresignedUrlPutRequest;

@RestController
@RequiredArgsConstructor
public class TestController {
@RequestMapping("/api/s3")
public class S3PresignedUrlController {
private final S3PresignedUrlService s3PresignedUrlService;

@GetMapping("/presigned-url-put")
public ResponseEntity<?> presignedUrlPut() {
String presignedPutUrl = s3PresignedUrlService.createPresignedPutUrl("poomasi", "test", null);
return ResponseEntity.ok(presignedPutUrl);
}
private final AwsProperties awsProperties;

@GetMapping("/presigned-url-get")
public ResponseEntity<?> presignedUrlGet(@RequestParam String keyname) {
String presignedGetUrl = s3PresignedUrlService.createPresignedGetUrl("poomasi", keyname);
String presignedGetUrl = s3PresignedUrlService.createPresignedGetUrl(awsProperties.getS3().getBucket(), keyname);
return ResponseEntity.ok(presignedGetUrl);
}

@PostMapping("/presigned-url-put")
public ResponseEntity<?> presignedUrlPut(@RequestBody PresignedUrlPutRequest request) {
String presignedPutUrl = s3PresignedUrlService.createPresignedPutUrl(awsProperties.getS3().getBucket(), request.keyPrefix(), request.metadata());
return ResponseEntity.ok(presignedPutUrl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.UUID;

@Service
@RequiredArgsConstructor
Expand All @@ -26,7 +27,6 @@ public class S3PresignedUrlService {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final Long SIGNATURE_DURATION = 10L;


public String createPresignedGetUrl(String bucketName, String keyName) {
GetObjectRequest objectRequest = GetObjectRequest.builder()
.bucket(bucketName)
Expand All @@ -51,8 +51,12 @@ public String createPresignedPutUrl(String bucketName, String keyPrefix, Map<Str
String date = now.format(DATE_FORMATTER);
String encodedTime = encryptionUtil.encodeTime(now).substring(0, 10);

String keyName = String.format("%s/%s/%s.jpg", keyPrefix, date, encodedTime);
// jpg 말고 다른 형식 파일 들어오는 경우에 대해서도 따로 처리 필요
// 주기적으로 s3 정리하는 스케줄러 구현 필요(사진 5개 이상이면 db에서 저장 안한것들은 지움)
// 극악의 확률로 url이 겹치면?? -> 그럴일 거의 없긴할텐데 생기면 s3 원래 파일 지워짐

String uniqueIdentifier = UUID.randomUUID().toString();
String keyName = String.format("%s/%s/%s_%s.jpg", keyPrefix, date, uniqueIdentifier, encodedTime);

PutObjectRequest objectRequest = PutObjectRequest.builder()
.bucket(bucketName)
Expand All @@ -65,16 +69,13 @@ public String createPresignedPutUrl(String bucketName, String keyPrefix, Map<Str
.putObjectRequest(objectRequest)
.build();


PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest);
String myURL = presignedRequest.url().toString();
log.info("Presigned URL to upload a file to: [{}]", myURL);
log.info("HTTP method: [{}]", presignedRequest.httpRequest().method());

return presignedRequest.url().toExternalForm();
}


}

// reference: https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/example_s3_Scenario_PresignedUrl_section.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package poomasi.global.config.s3.dto.request;

import java.util.Map;

public record PresignedUrlPutRequest(String keyPrefix, Map<String, String> metadata) {
}
7 changes: 6 additions & 1 deletion src/main/java/poomasi/global/error/BusinessError.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ public enum BusinessError {
RESERVATION_CANCELLATION_PERIOD_EXPIRED(HttpStatus.BAD_REQUEST, "예약 취소 기간이 지났습니다."),

// ETC
START_DATE_SHOULD_BE_BEFORE_END_DATE(HttpStatus.BAD_REQUEST, "시작 날짜는 종료 날짜보다 이전이어야 합니다.");
START_DATE_SHOULD_BE_BEFORE_END_DATE(HttpStatus.BAD_REQUEST, "시작 날짜는 종료 날짜보다 이전이어야 합니다."),

// Image
IMAGE_LIMIT_EXCEED(HttpStatus.BAD_REQUEST, "사진은 최대 5장까지 등록 가능합니다."),
IMAGE_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 이미지가 존재합니다"),
IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "이미지를 찾을 수 없습니다.");

private final HttpStatus httpStatus;

Expand Down