From 9a0c020039547cc7ac5ae2113929361d7410af1e Mon Sep 17 00:00:00 2001 From: jun02160 Date: Wed, 28 Feb 2024 14:11:06 +0900 Subject: [PATCH 01/11] =?UTF-8?q?[CHORE]=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=B2=84=ED=82=B7=EB=AA=85=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- umbba-api/src/main/resources/application-dev1.yml | 1 + umbba-api/src/main/resources/application-dev2.yml | 1 + umbba-api/src/main/resources/application-local.yml | 1 + umbba-api/src/main/resources/application-prod1.yml | 1 + umbba-api/src/main/resources/application-prod2.yml | 1 + 5 files changed, 5 insertions(+) diff --git a/umbba-api/src/main/resources/application-dev1.yml b/umbba-api/src/main/resources/application-dev1.yml index 1627e262..7bb35838 100644 --- a/umbba-api/src/main/resources/application-dev1.yml +++ b/umbba-api/src/main/resources/application-dev1.yml @@ -34,6 +34,7 @@ cloud: static: ${CLOUD_REGION_DEV} s3: bucket: ${BUCKET_NAME_DEV} + bucketImg: ${IMG_BUCKET_DEV} stack: auto: false sqs: diff --git a/umbba-api/src/main/resources/application-dev2.yml b/umbba-api/src/main/resources/application-dev2.yml index e5b79300..69a59002 100644 --- a/umbba-api/src/main/resources/application-dev2.yml +++ b/umbba-api/src/main/resources/application-dev2.yml @@ -34,6 +34,7 @@ cloud: static: ${CLOUD_REGION_DEV} s3: bucket: ${BUCKET_NAME_DEV} + bucketImg: ${IMG_BUCKET_DEV} stack: auto: false sqs: diff --git a/umbba-api/src/main/resources/application-local.yml b/umbba-api/src/main/resources/application-local.yml index 1ea9fcea..ee4d0e6e 100644 --- a/umbba-api/src/main/resources/application-local.yml +++ b/umbba-api/src/main/resources/application-local.yml @@ -34,6 +34,7 @@ cloud: static: ${CLOUD_REGION_LOCAL} s3: bucket: ${BUCKET_NAME_LOCAL} + bucketImg: ${IMG_BUCKET_LOCAL} stack: auto: false sqs: diff --git a/umbba-api/src/main/resources/application-prod1.yml b/umbba-api/src/main/resources/application-prod1.yml index 38b2c3d7..e2a14e28 100644 --- a/umbba-api/src/main/resources/application-prod1.yml +++ b/umbba-api/src/main/resources/application-prod1.yml @@ -34,6 +34,7 @@ cloud: static: ${CLOUD_REGION_PROD} s3: bucket: ${BUCKET_NAME_PROD} + bucketImg: ${IMG_BUCKET_PROD} stack: auto: false sqs: diff --git a/umbba-api/src/main/resources/application-prod2.yml b/umbba-api/src/main/resources/application-prod2.yml index 5d924d7f..bff4413a 100644 --- a/umbba-api/src/main/resources/application-prod2.yml +++ b/umbba-api/src/main/resources/application-prod2.yml @@ -34,6 +34,7 @@ cloud: static: ${CLOUD_REGION_PROD} s3: bucket: ${BUCKET_NAME_PROD} + bucketImg: ${IMG_BUCKET_PROD} stack: auto: false sqs: From c748518784c49ee3d6554dfd3962b1db2cee4265 Mon Sep 17 00:00:00 2001 From: jun02160 Date: Wed, 28 Feb 2024 14:51:06 +0900 Subject: [PATCH 02/11] =?UTF-8?q?[FEAT]=20Album=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=84=A4=EA=B3=84,=20=EC=83=88=EB=A1=9C=EC=9A=B4?= =?UTF-8?q?=20=EA=B8=B0=EB=A1=9D=20=EB=93=B1=EB=A1=9D=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?API=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/album/AlbumController.java | 39 ++++++++++++ .../dto/request/CreateAlbumRequestDto.java | 28 +++++++++ .../umbba/api/service/album/AlbumService.java | 48 ++++++++++++++ .../umbba/common/exception/SuccessType.java | 1 + .../org/umbba/domain/domain/album/Album.java | 62 +++++++++++++++++++ .../album/repository/AlbumRepository.java | 9 +++ 6 files changed, 187 insertions(+) create mode 100644 umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java create mode 100644 umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/request/CreateAlbumRequestDto.java create mode 100644 umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java create mode 100644 umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java create mode 100644 umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/repository/AlbumRepository.java diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java new file mode 100644 index 00000000..a0df1275 --- /dev/null +++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java @@ -0,0 +1,39 @@ +package sopt.org.umbba.api.controller.album; + +import static sopt.org.umbba.api.config.jwt.JwtProvider.*; +import static sopt.org.umbba.common.exception.SuccessType.*; + +import java.security.Principal; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import sopt.org.umbba.api.controller.album.dto.request.CreateAlbumRequestDto; +import sopt.org.umbba.api.service.album.AlbumService; +import sopt.org.umbba.common.exception.dto.ApiResponse; + +@RestController +@RequestMapping("/album") +@RequiredArgsConstructor +public class AlbumController { + + private final AlbumService albumService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse createAlbum(@Valid @RequestBody final CreateAlbumRequestDto request, final Principal principal, HttpServletResponse response) { + Long albumId = albumService.createAlbum(request, getUserFromPrincial(principal)); + response.setHeader("Location", "/album/" + albumId); + return ApiResponse.success(CREATE_ALBUM_SUCCESS); + } +} diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/request/CreateAlbumRequestDto.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/request/CreateAlbumRequestDto.java new file mode 100644 index 00000000..5b25abfb --- /dev/null +++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/request/CreateAlbumRequestDto.java @@ -0,0 +1,28 @@ +package sopt.org.umbba.api.controller.album.dto.request; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class CreateAlbumRequestDto { + + @NotBlank(message = "제목은 필수 입력 값입니다.") + @Size(max = 15) + private String title; + + @NotBlank(message = "소개글은 필수 입력 값입니다.") + @Size(max = 32) + private String content; + + @NotBlank(message = "이미지 파일명은 필수 입력 값입니다.") + private String imgFileName; +} diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java b/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java new file mode 100644 index 00000000..cdae62c0 --- /dev/null +++ b/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java @@ -0,0 +1,48 @@ +package sopt.org.umbba.api.service.album; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import sopt.org.umbba.api.controller.album.dto.request.CreateAlbumRequestDto; +import sopt.org.umbba.common.exception.ErrorType; +import sopt.org.umbba.common.exception.model.CustomException; +import sopt.org.umbba.domain.domain.album.Album; +import sopt.org.umbba.domain.domain.album.repository.AlbumRepository; +import sopt.org.umbba.domain.domain.parentchild.Parentchild; +import sopt.org.umbba.domain.domain.user.User; +import sopt.org.umbba.domain.domain.user.repository.UserRepository; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AlbumService { + + private final AlbumRepository albumRepository; + private final UserRepository userRepository; + + public Long createAlbum(final CreateAlbumRequestDto request, Long userId) { + + User user = getByUser(userId); + Parentchild parentchild = user.getParentChild(); + Album album = Album.builder() + .title(request.getTitle()) + .content(request.getContent()) + .imgUrl(request.getImgFileName()) + .username(user.getUsername()) + .parentchild(parentchild) + .build(); + albumRepository.save(album); + album.setParentchild(parentchild); + parentchild.addAlbum(album); + + return album.getId(); + } + + private User getByUser(Long userId) { + return userRepository.findById(userId).orElseThrow( + () -> new CustomException(ErrorType.INVALID_USER) + ); + } + +} diff --git a/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java b/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java index 14760146..fc8355e5 100644 --- a/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java +++ b/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java @@ -37,6 +37,7 @@ public enum SuccessType { CREATE_PARENT_CHILD_SUCCESS(HttpStatus.CREATED, "온보딩 정보를 입력받아 부모자식 관계를 생성하는 데 성공했습니다."), MATCH_PARENT_CHILD_SUCCESS(HttpStatus.CREATED, "부모자식 관계 매칭에 성공했습니다."), ANSWER_TODAY_QUESTION_SUCCESS(HttpStatus.CREATED, "오늘의 일일문답에 답변을 완료하였습니다."), + CREATE_ALBUM_SUCCESS(HttpStatus.CREATED, "앨범의 기록 생성에 성공했습니다."), ; diff --git a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java new file mode 100644 index 00000000..e66365be --- /dev/null +++ b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java @@ -0,0 +1,62 @@ +package sopt.org.umbba.domain.domain.album; + +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import sopt.org.umbba.domain.domain.common.AuditingTimeEntity; +import sopt.org.umbba.domain.domain.parentchild.Parentchild; +import sopt.org.umbba.domain.domain.user.User; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Album extends AuditingTimeEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "album_id") + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + @Column(columnDefinition = "TEXT") + private String imgUrl; + + @Column(nullable = false) + private String username; + + @ManyToOne + @JoinColumn(name = "parentchild_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + private Parentchild parentchild; + + @Builder + private Album(String title, String content, String imgUrl, String username, Parentchild parentchild) { + this.title = title; + this.content = content; + this.imgUrl = imgUrl; + this.username = username; + this.parentchild = parentchild; + } + + public void setParentchild(Parentchild parentchild) { + this.parentchild = parentchild; + + if (!parentchild.getAlbumList().contains(this)) { + parentchild.getAlbumList().add(this); + } + } +} diff --git a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/repository/AlbumRepository.java b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/repository/AlbumRepository.java new file mode 100644 index 00000000..33a84927 --- /dev/null +++ b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/repository/AlbumRepository.java @@ -0,0 +1,9 @@ +package sopt.org.umbba.domain.domain.album.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import sopt.org.umbba.domain.domain.album.Album; + +public interface AlbumRepository extends JpaRepository { + +} From 142e461b77b7c0b3bb57f775d6e579938a81c316 Mon Sep 17 00:00:00 2001 From: jun02160 Date: Wed, 28 Feb 2024 16:01:50 +0900 Subject: [PATCH 03/11] =?UTF-8?q?[FIX]=20JPA=20Entity=20=EC=97=B0=EA=B4=80?= =?UTF-8?q?=EA=B4=80=EA=B3=84=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umbba/domain/domain/parentchild/Parentchild.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/parentchild/Parentchild.java b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/parentchild/Parentchild.java index 31a19aa2..a1a03e48 100644 --- a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/parentchild/Parentchild.java +++ b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/parentchild/Parentchild.java @@ -6,6 +6,7 @@ import org.hibernate.annotations.Where; import sopt.org.umbba.common.exception.ErrorType; import sopt.org.umbba.common.exception.model.CustomException; +import sopt.org.umbba.domain.domain.album.Album; import sopt.org.umbba.domain.domain.common.AuditingTimeEntity; import sopt.org.umbba.domain.domain.qna.OnboardingAnswer; import sopt.org.umbba.domain.domain.qna.QnA; @@ -34,6 +35,9 @@ public class Parentchild extends AuditingTimeEntity { @JoinColumn(name = "parentchild_id") private List qnaList; + @OneToMany(mappedBy = "parentchild", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + private final List albumList = new ArrayList<>(); + @Column(nullable = false) private int count; @@ -100,4 +104,11 @@ public void addQna(QnA qnA) { qnaList.add(qnA); } + public void addAlbum(Album album) { + this.albumList.add(album); + if (album.getParentchild() != this) { + album.setParentchild(this); + } + } + } From 126dfa47f9d3ca5f3fa4f6e20fa0f8d834895144 Mon Sep 17 00:00:00 2001 From: jun02160 Date: Wed, 28 Feb 2024 16:04:38 +0900 Subject: [PATCH 04/11] =?UTF-8?q?[FEAT]=20AWS=20S3=20=EC=84=A4=EC=A0=95,?= =?UTF-8?q?=20PreSigned=20Url=20=EB=B0=9C=EA=B8=89=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/umbba/common/exception/ErrorType.java | 5 + .../umbba/common/exception/SuccessType.java | 2 + umbba-external/build.gradle | 4 + .../umbba/external/s3/PreSignedUrlDto.java | 17 +++ .../org/umbba/external/s3/S3BucketPrefix.java | 25 +++++ .../sopt/org/umbba/external/s3/S3Service.java | 103 ++++++++++++++++++ .../umbba/external/s3/config/S3Config.java | 57 ++++++++++ 7 files changed, 213 insertions(+) create mode 100644 umbba-external/src/main/java/sopt/org/umbba/external/s3/PreSignedUrlDto.java create mode 100644 umbba-external/src/main/java/sopt/org/umbba/external/s3/S3BucketPrefix.java create mode 100644 umbba-external/src/main/java/sopt/org/umbba/external/s3/S3Service.java create mode 100644 umbba-external/src/main/java/sopt/org/umbba/external/s3/config/S3Config.java diff --git a/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java b/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java index 37bb4846..a5190aa4 100644 --- a/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java +++ b/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java @@ -34,6 +34,8 @@ public enum ErrorType { ALREADY_EXISTS_PARENT_CHILD_USER(HttpStatus.BAD_REQUEST, "이미 해당 유저의 부모자식 관계가 존재합니다."), ALREADY_QNA_LIST_FULL(HttpStatus.BAD_REQUEST, "이미 QNA 리스트가 가득 찼습니다"), + // Album + INVALID_BUCKET_PREFIX(HttpStatus.BAD_REQUEST, "유효하지 않은 S3 버킷 디렉토리명입니다."), /** * 401 UNAUTHORIZED @@ -80,6 +82,9 @@ public enum ErrorType { DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "데이터베이스 관련 에러가 발생했습니다."), FIREBASE_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파이어베이스 서버와의 연결에 실패했습니다."), FAIL_TO_SEND_PUSH_ALARM(HttpStatus.INTERNAL_SERVER_ERROR, "푸시 알림 메세지 전송에 실패했습니다."), + FAIL_TO_GET_IMAGE_PRE_SIGNED_URL(HttpStatus.INTERNAL_SERVER_ERROR, "PreSigned Url을 가져오는 데 실패했습니다."), + FAIL_TO_DELETE_IMAGE(HttpStatus.INTERNAL_SERVER_ERROR, "S3 버킷에서 이미지를 삭제하는 데 실패했습니다."), + S3_BUCKET_GET_IMAGE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "이미지를 불러오는 데 실패했습니다."), // ETC INDEX_OUT_OF_BOUNDS(HttpStatus.INTERNAL_SERVER_ERROR, "인덱스 범위를 초과했습니다."), diff --git a/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java b/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java index fc8355e5..ec21da41 100644 --- a/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java +++ b/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java @@ -29,6 +29,8 @@ public enum SuccessType { TEST_SUCCESS(HttpStatus.OK, "데모데이 테스트용 API 호출에 성공했습니다."), RESTART_QNA_SUCCESS(HttpStatus.OK, "7일 이후 문답이 정상적으로 시작되었습니다."), GET_USER_FIRST_ENTRY_SUCCESS(HttpStatus.OK, "유저의 첫 진입여부 조회에 성공했습니다."), + GET_PRE_SIGNED_URL_SUCCESS(HttpStatus.OK, "PreSigned Url 조회에 성공했습니다."), + IMAGE_S3_DELETE_SUCCESS(HttpStatus.OK, "S3 버킷에서 이미지를 삭제하는 데 성공했습니다."), /** diff --git a/umbba-external/build.gradle b/umbba-external/build.gradle index 1bc7fa04..07adf909 100644 --- a/umbba-external/build.gradle +++ b/umbba-external/build.gradle @@ -10,6 +10,10 @@ dependencies { // for JsonIgnore implementation group: "io.jsonwebtoken", name: "jjwt-jackson", version: "0.11.2" + // AWS sdk + implementation("software.amazon.awssdk:bom:2.21.0") + implementation("software.amazon.awssdk:s3:2.21.0") + // spring webflux // implementation "org.springframework.boot:spring-boot-starter-webflux" } \ No newline at end of file diff --git a/umbba-external/src/main/java/sopt/org/umbba/external/s3/PreSignedUrlDto.java b/umbba-external/src/main/java/sopt/org/umbba/external/s3/PreSignedUrlDto.java new file mode 100644 index 00000000..d29775f4 --- /dev/null +++ b/umbba-external/src/main/java/sopt/org/umbba/external/s3/PreSignedUrlDto.java @@ -0,0 +1,17 @@ +package sopt.org.umbba.external.s3; + +import lombok.Builder; + +@Builder +public class PreSignedUrlDto { + + private String fileName; + private String url; + + public static PreSignedUrlDto of(String fileName, String url) { + return PreSignedUrlDto.builder() + .fileName(fileName) + .url(url) + .build(); + } +} diff --git a/umbba-external/src/main/java/sopt/org/umbba/external/s3/S3BucketPrefix.java b/umbba-external/src/main/java/sopt/org/umbba/external/s3/S3BucketPrefix.java new file mode 100644 index 00000000..5c1b3176 --- /dev/null +++ b/umbba-external/src/main/java/sopt/org/umbba/external/s3/S3BucketPrefix.java @@ -0,0 +1,25 @@ +package sopt.org.umbba.external.s3; + +import static sopt.org.umbba.common.exception.ErrorType.*; + +import java.util.Arrays; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import sopt.org.umbba.common.exception.model.CustomException; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum S3BucketPrefix { + ALBUM_PREFIX("album/"); + + private final String value; + + public static S3BucketPrefix of(String value) { + return Arrays.stream(S3BucketPrefix.values()) + .filter(prefix -> value.equals(prefix.value)) + .findFirst() + .orElseThrow(() -> new CustomException(INVALID_BUCKET_PREFIX)); + } +} diff --git a/umbba-external/src/main/java/sopt/org/umbba/external/s3/S3Service.java b/umbba-external/src/main/java/sopt/org/umbba/external/s3/S3Service.java new file mode 100644 index 00000000..453bb7c0 --- /dev/null +++ b/umbba-external/src/main/java/sopt/org/umbba/external/s3/S3Service.java @@ -0,0 +1,103 @@ +package sopt.org.umbba.external.s3; + +import static sopt.org.umbba.common.exception.ErrorType.*; + +import java.net.URL; +import java.time.Duration; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.GetUrlRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; +import sopt.org.umbba.common.exception.model.CustomException; + +@Slf4j +@Component +public class S3Service { + + private static final Long PRE_SIGNED_URL_EXPIRE_MINUTE = 1L; // 만료시간 1분 + private static final String IMAGE_EXTENSION = ".jpg"; + + private final String bucketName; + + private final S3Client s3Client; + private final S3Presigner s3Presigner; + + public S3Service(@Value("${cloud.aws.s3.bucketImg}") final String bucketName, final S3Client s3Client, final S3Presigner s3Presigner) { + this.bucketName = bucketName; + this.s3Client = s3Client; + this.s3Presigner = s3Presigner; + } + + // 이미지 저장을 위한 PreSigned Url 발급 + public PreSignedUrlDto getPreSignedUrl(final S3BucketPrefix prefix) { + final String fileName = generateImageFileName(); // UUID 문자열 + final String key = prefix.getValue() + fileName; + + try { + PutObjectRequest request = PutObjectRequest.builder() + .bucket(bucketName) + .key(key).build(); + + PutObjectPresignRequest preSignedUrlRequest = PutObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(PRE_SIGNED_URL_EXPIRE_MINUTE)) + .putObjectRequest(request).build(); + + String url = s3Presigner.presignPutObject(preSignedUrlRequest).url().toString(); + return PreSignedUrlDto.of(fileName, url); + } catch (RuntimeException e) { + throw new CustomException(FAIL_TO_GET_IMAGE_PRE_SIGNED_URL); + } + } + + private String generateImageFileName() { + return UUID.randomUUID() + IMAGE_EXTENSION; + } + + // S3 버킷으로부터 이미지 삭제 + public void deleteImage(String key) { + try { + s3Client.deleteObject((DeleteObjectRequest.Builder builder) -> + builder.bucket(bucketName) + .key(key).build()); + } catch (RuntimeException e) { + throw new CustomException(FAIL_TO_DELETE_IMAGE); + } + } + + // 파일명으로부터 S3 Bucket URL 조회 + public String getS3ImgUrl(String prefix, String fileName) { + + String imageKey = prefix + fileName; + + try { + GetUrlRequest request = GetUrlRequest.builder() + .bucket(bucketName) + .key(imageKey) + .build(); + + URL imageUrl = s3Client.utilities().getUrl(request); + + String urlWithKey = "https://" + bucketName + ".s3.ap-northeast-2.amazonaws.com/" + imageKey; + if (urlWithKey.equals(imageUrl.toString())) { + log.info("S3에 저장된 이미지 Url: {}", imageUrl); + return imageUrl.toString(); + } + throw new CustomException(S3_BUCKET_GET_IMAGE_ERROR); + } catch (S3Exception e) { + throw new CustomException(S3_BUCKET_GET_IMAGE_ERROR); + } + } + + private String getUrlByFileName(String prefix, String fileName) { + return "https://"+bucketName+".s3.ap-northeast-2.amazonaws.com/"+prefix+fileName; + } +} diff --git a/umbba-external/src/main/java/sopt/org/umbba/external/s3/config/S3Config.java b/umbba-external/src/main/java/sopt/org/umbba/external/s3/config/S3Config.java new file mode 100644 index 00000000..3131612c --- /dev/null +++ b/umbba-external/src/main/java/sopt/org/umbba/external/s3/config/S3Config.java @@ -0,0 +1,57 @@ +package sopt.org.umbba.external.s3.config; + +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.SystemPropertyCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; + +@Configuration +public class S3Config { + + private static String AWS_ACCESS_KEY_ID = "aws.accessKeyId"; + private static String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey"; + + private final String accessKey; + private final String secretKey; + private final String regionString; + + public S3Config(@Value("${cloud.aws.credentials.accessKey}") final String accessKey, + @Value("${cloud.aws.credentials.secretKey}") final String secretKey, + @Value("${cloud.aws.region.static}") final String regionString) { + this.accessKey = accessKey; + this.secretKey = secretKey; + this.regionString = regionString; + } + + @Bean + public SystemPropertyCredentialsProvider systemPropertyCredentialsProvider() { + System.setProperty(AWS_ACCESS_KEY_ID, accessKey); + System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey); + return SystemPropertyCredentialsProvider.create(); + } + + @Bean + public Region getRegion() { + return Region.of(regionString); + } + + @Bean + public S3Client getS3Client() { + return S3Client.builder() + .region(getRegion()) + .credentialsProvider(systemPropertyCredentialsProvider()) + .build(); + } + + @Bean + public S3Presigner getS3PreSigner() { + return S3Presigner.builder() + .region(getRegion()) + .credentialsProvider(systemPropertyCredentialsProvider()) + .build(); + } +} \ No newline at end of file From 0ba13af12a0995b8df23f4d9f37d2f4bc2366ee3 Mon Sep 17 00:00:00 2001 From: jun02160 Date: Wed, 28 Feb 2024 16:04:58 +0900 Subject: [PATCH 05/11] =?UTF-8?q?[FEAT]=20PreSigned=20Url=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/album/AlbumController.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java index a0df1275..f31403bf 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java @@ -3,17 +3,19 @@ import static sopt.org.umbba.api.config.jwt.JwtProvider.*; import static sopt.org.umbba.common.exception.SuccessType.*; +import java.io.IOException; import java.security.Principal; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -21,6 +23,9 @@ import sopt.org.umbba.api.controller.album.dto.request.CreateAlbumRequestDto; import sopt.org.umbba.api.service.album.AlbumService; import sopt.org.umbba.common.exception.dto.ApiResponse; +import sopt.org.umbba.external.s3.PreSignedUrlDto; +import sopt.org.umbba.external.s3.S3BucketPrefix; +import sopt.org.umbba.external.s3.S3Service; @RestController @RequestMapping("/album") @@ -28,6 +33,7 @@ public class AlbumController { private final AlbumService albumService; + private final S3Service s3Service; @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -36,4 +42,17 @@ public ApiResponse createAlbum(@Valid @RequestBody final CreateAlbumRequestDto r response.setHeader("Location", "/album/" + albumId); return ApiResponse.success(CREATE_ALBUM_SUCCESS); } -} + + // PreSigned Url 이용 (클라이언트에서 해당 URL로 업로드) + @PatchMapping("/image") + public ApiResponse getImgPreSignedUrl(@RequestParam("img_prefix") String imgPrefix) throws IOException { + return ApiResponse.success(GET_PRE_SIGNED_URL_SUCCESS, s3Service.getPreSignedUrl(S3BucketPrefix.valueOf(imgPrefix))); + } + + // 버킷에서 이미지 삭제 TODO 내부 로직으로 뺄 예정 + @DeleteMapping("/image") + public ApiResponse deleteImage(@RequestParam("img_prefix") String imgPrefix) throws IOException { + s3Service.deleteImage(imgPrefix); + return ApiResponse.success(IMAGE_S3_DELETE_SUCCESS); + } +} \ No newline at end of file From 2f4f1bd0f314d53d55cbb85daaf75ef6c809e2d8 Mon Sep 17 00:00:00 2001 From: jun02160 Date: Wed, 28 Feb 2024 16:45:37 +0900 Subject: [PATCH 06/11] =?UTF-8?q?[FIX]=20@RequestParam=20->=20@RequestBody?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/album/AlbumController.java | 12 ++++++++---- .../album/dto/request/AlbumImgUrlRequestDto.java | 16 ++++++++++++++++ .../umbba/api/service/album/AlbumService.java | 4 ++-- .../java/sopt/org/umbba/api/service/example.txt | 1 - .../org/umbba/external/s3/PreSignedUrlDto.java | 6 ++++++ 5 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/request/AlbumImgUrlRequestDto.java delete mode 100644 umbba-api/src/main/java/sopt/org/umbba/api/service/example.txt diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java index f31403bf..c536ca48 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java @@ -2,6 +2,7 @@ import static sopt.org.umbba.api.config.jwt.JwtProvider.*; import static sopt.org.umbba.common.exception.SuccessType.*; +import static sopt.org.umbba.external.s3.S3BucketPrefix.*; import java.io.IOException; import java.security.Principal; @@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; +import sopt.org.umbba.api.controller.album.dto.request.AlbumImgUrlRequestDto; import sopt.org.umbba.api.controller.album.dto.request.CreateAlbumRequestDto; import sopt.org.umbba.api.service.album.AlbumService; import sopt.org.umbba.common.exception.dto.ApiResponse; @@ -38,20 +40,22 @@ public class AlbumController { @PostMapping @ResponseStatus(HttpStatus.CREATED) public ApiResponse createAlbum(@Valid @RequestBody final CreateAlbumRequestDto request, final Principal principal, HttpServletResponse response) { - Long albumId = albumService.createAlbum(request, getUserFromPrincial(principal)); + String imgUrl = s3Service.getS3ImgUrl(ALBUM_PREFIX.getValue(), request.getImgFileName()); + Long albumId = albumService.createAlbum(request, imgUrl, getUserFromPrincial(principal)); response.setHeader("Location", "/album/" + albumId); return ApiResponse.success(CREATE_ALBUM_SUCCESS); } // PreSigned Url 이용 (클라이언트에서 해당 URL로 업로드) @PatchMapping("/image") - public ApiResponse getImgPreSignedUrl(@RequestParam("img_prefix") String imgPrefix) throws IOException { - return ApiResponse.success(GET_PRE_SIGNED_URL_SUCCESS, s3Service.getPreSignedUrl(S3BucketPrefix.valueOf(imgPrefix))); + @ResponseStatus(HttpStatus.OK) + public ApiResponse getImgPreSignedUrl(@RequestBody final AlbumImgUrlRequestDto request) { + return ApiResponse.success(GET_PRE_SIGNED_URL_SUCCESS, s3Service.getPreSignedUrl(S3BucketPrefix.of(request.getImgPrefix()))); } // 버킷에서 이미지 삭제 TODO 내부 로직으로 뺄 예정 @DeleteMapping("/image") - public ApiResponse deleteImage(@RequestParam("img_prefix") String imgPrefix) throws IOException { + public ApiResponse deleteImage(@RequestParam("img_prefix") String imgPrefix) { s3Service.deleteImage(imgPrefix); return ApiResponse.success(IMAGE_S3_DELETE_SUCCESS); } diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/request/AlbumImgUrlRequestDto.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/request/AlbumImgUrlRequestDto.java new file mode 100644 index 00000000..669e7126 --- /dev/null +++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/request/AlbumImgUrlRequestDto.java @@ -0,0 +1,16 @@ +package sopt.org.umbba.api.controller.album.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AlbumImgUrlRequestDto { + + private String imgPrefix; +} diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java b/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java index cdae62c0..226feb27 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java @@ -21,14 +21,14 @@ public class AlbumService { private final AlbumRepository albumRepository; private final UserRepository userRepository; - public Long createAlbum(final CreateAlbumRequestDto request, Long userId) { + public Long createAlbum(final CreateAlbumRequestDto request, String imgUrl, Long userId) { User user = getByUser(userId); Parentchild parentchild = user.getParentChild(); Album album = Album.builder() .title(request.getTitle()) .content(request.getContent()) - .imgUrl(request.getImgFileName()) + .imgUrl(imgUrl) .username(user.getUsername()) .parentchild(parentchild) .build(); diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/service/example.txt b/umbba-api/src/main/java/sopt/org/umbba/api/service/example.txt deleted file mode 100644 index 945c9b46..00000000 --- a/umbba-api/src/main/java/sopt/org/umbba/api/service/example.txt +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/umbba-external/src/main/java/sopt/org/umbba/external/s3/PreSignedUrlDto.java b/umbba-external/src/main/java/sopt/org/umbba/external/s3/PreSignedUrlDto.java index d29775f4..0f5554fe 100644 --- a/umbba-external/src/main/java/sopt/org/umbba/external/s3/PreSignedUrlDto.java +++ b/umbba-external/src/main/java/sopt/org/umbba/external/s3/PreSignedUrlDto.java @@ -1,8 +1,14 @@ package sopt.org.umbba.external.s3; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + import lombok.Builder; +import lombok.Getter; @Builder +@Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class PreSignedUrlDto { private String fileName; From 39926b3767a348b98d582183a7c82be075cdc154 Mon Sep 17 00:00:00 2001 From: jun02160 Date: Wed, 28 Feb 2024 17:20:39 +0900 Subject: [PATCH 07/11] =?UTF-8?q?[FEAT]=20=EC=95=A8=EB=B2=94=20=EA=B8=80?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20API=20=EA=B5=AC=ED=98=84=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/umbba/api/config/SecurityConfig.java | 3 ++- .../api/controller/album/AlbumController.java | 13 ++++++++-- .../umbba/api/service/album/AlbumService.java | 25 ++++++++++++++++--- .../org/umbba/common/exception/ErrorType.java | 1 + .../umbba/common/exception/SuccessType.java | 1 + .../org/umbba/domain/domain/album/Album.java | 4 +++ .../domain/parentchild/Parentchild.java | 6 +++++ .../sopt/org/umbba/external/s3/S3Service.java | 18 ++++++++++--- 8 files changed, 62 insertions(+), 9 deletions(-) diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/SecurityConfig.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/SecurityConfig.java index 20fdfdc5..9f3c1bed 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/config/SecurityConfig.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/SecurityConfig.java @@ -25,7 +25,8 @@ public class SecurityConfig { // "/log-out", "/test", "/profile", "/health", "/actuator/health", "/alarm/qna", "/alarm/drink", - "/demo/**" + "/demo/**", + "/album/image" }; @Bean diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java index c536ca48..f0698dee 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java @@ -13,6 +13,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; 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.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -53,10 +54,18 @@ public ApiResponse getImgPreSignedUrl(@RequestBody final AlbumI return ApiResponse.success(GET_PRE_SIGNED_URL_SUCCESS, s3Service.getPreSignedUrl(S3BucketPrefix.of(request.getImgPrefix()))); } + @DeleteMapping + @ResponseStatus(HttpStatus.OK) + public ApiResponse deleteAlbum(@PathVariable final Long albumId, final Principal principal) { + String imgUrl = albumService.deleteAlbum(albumId, getUserFromPrincial(principal)); + s3Service.deleteS3Image(imgUrl); + return ApiResponse.success(DELETE_ALBUM_SUCCESS); + } + // 버킷에서 이미지 삭제 TODO 내부 로직으로 뺄 예정 @DeleteMapping("/image") - public ApiResponse deleteImage(@RequestParam("img_prefix") String imgPrefix) { - s3Service.deleteImage(imgPrefix); + public ApiResponse deleteImage(@RequestParam("img_url") String imgUrl) { + s3Service.deleteS3Image(imgUrl); return ApiResponse.success(IMAGE_S3_DELETE_SUCCESS); } } \ No newline at end of file diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java b/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java index 226feb27..d7c8d91f 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java @@ -21,9 +21,10 @@ public class AlbumService { private final AlbumRepository albumRepository; private final UserRepository userRepository; - public Long createAlbum(final CreateAlbumRequestDto request, String imgUrl, Long userId) { + @Transactional + public Long createAlbum(final CreateAlbumRequestDto request, final String imgUrl, final Long userId) { - User user = getByUser(userId); + User user = getUserById(userId); Parentchild parentchild = user.getParentChild(); Album album = Album.builder() .title(request.getTitle()) @@ -39,10 +40,28 @@ public Long createAlbum(final CreateAlbumRequestDto request, String imgUrl, Long return album.getId(); } - private User getByUser(Long userId) { + @Transactional + public String deleteAlbum(final Long albumId, final Long userId) { + + User user = getUserById(userId); + Album album = getAlbumById(albumId); + + album.deleteParentchild(); + user.getParentChild().deleteAlbum(album); + albumRepository.delete(album); + + return album.getImgUrl(); + } + + private User getUserById(Long userId) { return userRepository.findById(userId).orElseThrow( () -> new CustomException(ErrorType.INVALID_USER) ); } + private Album getAlbumById(Long albumId) { + return albumRepository.findById(albumId).orElseThrow( + () -> new CustomException(ErrorType.NOT_FOUND_ALBUM) + ); + } } diff --git a/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java b/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java index a5190aa4..9e237040 100644 --- a/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java +++ b/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java @@ -63,6 +63,7 @@ public enum ErrorType { PARENTCHILD_HAVE_NO_QNALIST(HttpStatus.NOT_FOUND, "부모자식 관계가 가지고 있는 QnA 데이터가 없습니다."), PARENTCHILD_HAVE_NO_OPPONENT(HttpStatus.NOT_FOUND, "부모자식 관계에 1명만 참여하고 있습니다."), NOT_FOUND_SECTION(HttpStatus.NOT_FOUND, "해당 아이디와 일치하는 섹션이 없습니다."), + NOT_FOUND_ALBUM(HttpStatus.NOT_FOUND, "존재하지 않는 앨범입니다."), /** * About Apple (HttpStatus 고민) diff --git a/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java b/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java index ec21da41..006a913b 100644 --- a/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java +++ b/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java @@ -31,6 +31,7 @@ public enum SuccessType { GET_USER_FIRST_ENTRY_SUCCESS(HttpStatus.OK, "유저의 첫 진입여부 조회에 성공했습니다."), GET_PRE_SIGNED_URL_SUCCESS(HttpStatus.OK, "PreSigned Url 조회에 성공했습니다."), IMAGE_S3_DELETE_SUCCESS(HttpStatus.OK, "S3 버킷에서 이미지를 삭제하는 데 성공했습니다."), + DELETE_ALBUM_SUCCESS(HttpStatus.OK, "앨범의 기록 삭제에 성공했습니다."), /** diff --git a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java index e66365be..70e3cac8 100644 --- a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java +++ b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java @@ -59,4 +59,8 @@ public void setParentchild(Parentchild parentchild) { parentchild.getAlbumList().add(this); } } + + public void deleteParentchild() { + this.parentchild = null; + } } diff --git a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/parentchild/Parentchild.java b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/parentchild/Parentchild.java index a1a03e48..7a88e0fd 100644 --- a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/parentchild/Parentchild.java +++ b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/parentchild/Parentchild.java @@ -111,4 +111,10 @@ public void addAlbum(Album album) { } } + public void deleteAlbum(Album album) { + if (this.albumList.contains(album)) { + this.albumList.remove(album); + } + } + } diff --git a/umbba-external/src/main/java/sopt/org/umbba/external/s3/S3Service.java b/umbba-external/src/main/java/sopt/org/umbba/external/s3/S3Service.java index 453bb7c0..6a21a903 100644 --- a/umbba-external/src/main/java/sopt/org/umbba/external/s3/S3Service.java +++ b/umbba-external/src/main/java/sopt/org/umbba/external/s3/S3Service.java @@ -25,6 +25,7 @@ public class S3Service { private static final Long PRE_SIGNED_URL_EXPIRE_MINUTE = 1L; // 만료시간 1분 private static final String IMAGE_EXTENSION = ".jpg"; + private static final String AWS_DOMAIN = "amazonaws.com/"; private final String bucketName; @@ -63,7 +64,8 @@ private String generateImageFileName() { } // S3 버킷으로부터 이미지 삭제 - public void deleteImage(String key) { + public void deleteS3Image(String url) { + String key = getKeyByUrl(url); try { s3Client.deleteObject((DeleteObjectRequest.Builder builder) -> builder.bucket(bucketName) @@ -97,7 +99,17 @@ public String getS3ImgUrl(String prefix, String fileName) { } } - private String getUrlByFileName(String prefix, String fileName) { - return "https://"+bucketName+".s3.ap-northeast-2.amazonaws.com/"+prefix+fileName; + private String getKeyByUrl(String imgUrl) { + + int index = imgUrl.indexOf(AWS_DOMAIN); + String imageKey = ""; + if (index != -1) { + imageKey = imgUrl.substring(index + AWS_DOMAIN.length()); + log.info("imageKey substring으로 가져옴: {}", imageKey); + } else { + log.error("imageKey substring으로 가져오기 실패"); + } + + return imageKey; } } From 6434a2aa3a0f1c7bd0de92d5532d6e920ff1b509 Mon Sep 17 00:00:00 2001 From: jun02160 Date: Wed, 28 Feb 2024 21:52:50 +0900 Subject: [PATCH 08/11] =?UTF-8?q?[FEAT]=20=EC=95=A8=EB=B2=94=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20#13?= =?UTF-8?q?3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/album/AlbumController.java | 12 ++++++-- .../album/dto/response/AlbumResponseDto.java | 28 +++++++++++++++++++ .../umbba/api/service/album/AlbumService.java | 18 ++++++++++-- .../umbba/common/exception/SuccessType.java | 1 + .../org/umbba/domain/domain/album/Album.java | 6 ++-- .../album/repository/AlbumRepository.java | 5 ++++ 6 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/response/AlbumResponseDto.java diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java index f0698dee..1d224103 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java @@ -4,14 +4,15 @@ import static sopt.org.umbba.common.exception.SuccessType.*; import static sopt.org.umbba.external.s3.S3BucketPrefix.*; -import java.io.IOException; import java.security.Principal; +import java.util.List; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; +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; @@ -24,6 +25,7 @@ import lombok.RequiredArgsConstructor; import sopt.org.umbba.api.controller.album.dto.request.AlbumImgUrlRequestDto; import sopt.org.umbba.api.controller.album.dto.request.CreateAlbumRequestDto; +import sopt.org.umbba.api.controller.album.dto.response.AlbumResponseDto; import sopt.org.umbba.api.service.album.AlbumService; import sopt.org.umbba.common.exception.dto.ApiResponse; import sopt.org.umbba.external.s3.PreSignedUrlDto; @@ -54,7 +56,7 @@ public ApiResponse getImgPreSignedUrl(@RequestBody final AlbumI return ApiResponse.success(GET_PRE_SIGNED_URL_SUCCESS, s3Service.getPreSignedUrl(S3BucketPrefix.of(request.getImgPrefix()))); } - @DeleteMapping + @DeleteMapping("/{albumId}") @ResponseStatus(HttpStatus.OK) public ApiResponse deleteAlbum(@PathVariable final Long albumId, final Principal principal) { String imgUrl = albumService.deleteAlbum(albumId, getUserFromPrincial(principal)); @@ -62,6 +64,12 @@ public ApiResponse deleteAlbum(@PathVariable final Long albumId, final Principal return ApiResponse.success(DELETE_ALBUM_SUCCESS); } + @GetMapping + @ResponseStatus(HttpStatus.OK) + public ApiResponse> getAlbumList(final Principal principal) { + return ApiResponse.success(GET_ALBUM_LIST_SUCCESS, albumService.getAlbumList(getUserFromPrincial(principal))); + } + // 버킷에서 이미지 삭제 TODO 내부 로직으로 뺄 예정 @DeleteMapping("/image") public ApiResponse deleteImage(@RequestParam("img_url") String imgUrl) { diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/response/AlbumResponseDto.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/response/AlbumResponseDto.java new file mode 100644 index 00000000..f56502c1 --- /dev/null +++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/response/AlbumResponseDto.java @@ -0,0 +1,28 @@ +package sopt.org.umbba.api.controller.album.dto.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import lombok.Builder; +import lombok.Getter; +import sopt.org.umbba.domain.domain.album.Album; + +@Getter +@Builder +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AlbumResponseDto { + + private String title; + private String content; + private String writer; + private String imgUrl; + + public static AlbumResponseDto of(Album album) { + return AlbumResponseDto.builder() + .title(album.getTitle()) + .content(album.getContent()) + .writer(album.getWriter()) + .imgUrl(album.getImgUrl()) + .build(); + } +} diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java b/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java index d7c8d91f..5803c099 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java @@ -1,10 +1,14 @@ package sopt.org.umbba.api.service.album; +import java.util.List; +import java.util.stream.Collectors; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import sopt.org.umbba.api.controller.album.dto.request.CreateAlbumRequestDto; +import sopt.org.umbba.api.controller.album.dto.response.AlbumResponseDto; import sopt.org.umbba.common.exception.ErrorType; import sopt.org.umbba.common.exception.model.CustomException; import sopt.org.umbba.domain.domain.album.Album; @@ -30,7 +34,7 @@ public Long createAlbum(final CreateAlbumRequestDto request, final String imgUrl .title(request.getTitle()) .content(request.getContent()) .imgUrl(imgUrl) - .username(user.getUsername()) + .writer(user.getUsername()) .parentchild(parentchild) .build(); albumRepository.save(album); @@ -53,7 +57,17 @@ public String deleteAlbum(final Long albumId, final Long userId) { return album.getImgUrl(); } - private User getUserById(Long userId) { + public List getAlbumList(final Long userId) { + User user = getUserById(userId); + List albumList = albumRepository.findAllByParentchildOrderByCreatedAtDesc( + user.getParentChild()); + + return albumList.stream() + .map(AlbumResponseDto::of) + .collect(Collectors.toList()); + } + + private User getUserById(Long userId) { // TODO userId -> Parentchild 한번에 가져오기 return userRepository.findById(userId).orElseThrow( () -> new CustomException(ErrorType.INVALID_USER) ); diff --git a/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java b/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java index 006a913b..6761ac19 100644 --- a/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java +++ b/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java @@ -32,6 +32,7 @@ public enum SuccessType { GET_PRE_SIGNED_URL_SUCCESS(HttpStatus.OK, "PreSigned Url 조회에 성공했습니다."), IMAGE_S3_DELETE_SUCCESS(HttpStatus.OK, "S3 버킷에서 이미지를 삭제하는 데 성공했습니다."), DELETE_ALBUM_SUCCESS(HttpStatus.OK, "앨범의 기록 삭제에 성공했습니다."), + GET_ALBUM_LIST_SUCCESS(HttpStatus.OK, "앨범의 기록 목록 조회에 성공했습니다."), /** diff --git a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java index 70e3cac8..342cccb3 100644 --- a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java +++ b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/Album.java @@ -37,18 +37,18 @@ public class Album extends AuditingTimeEntity { private String imgUrl; @Column(nullable = false) - private String username; + private String writer; @ManyToOne @JoinColumn(name = "parentchild_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Parentchild parentchild; @Builder - private Album(String title, String content, String imgUrl, String username, Parentchild parentchild) { + private Album(String title, String content, String imgUrl, String writer, Parentchild parentchild) { this.title = title; this.content = content; this.imgUrl = imgUrl; - this.username = username; + this.writer = writer; this.parentchild = parentchild; } diff --git a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/repository/AlbumRepository.java b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/repository/AlbumRepository.java index 33a84927..ec073f38 100644 --- a/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/repository/AlbumRepository.java +++ b/umbba-domain/src/main/java/sopt/org/umbba/domain/domain/album/repository/AlbumRepository.java @@ -1,9 +1,14 @@ package sopt.org.umbba.domain.domain.album.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import sopt.org.umbba.domain.domain.album.Album; +import sopt.org.umbba.domain.domain.parentchild.Parentchild; public interface AlbumRepository extends JpaRepository { + List findAllByParentchildOrderByCreatedAtDesc(Parentchild parentchild); + } From 9dd657c3775edbc2b368a2a41d65de69cfc62bf3 Mon Sep 17 00:00:00 2001 From: jun02160 Date: Thu, 29 Feb 2024 00:03:05 +0900 Subject: [PATCH 09/11] =?UTF-8?q?[DEL]=20=EB=B2=84=ED=82=B7=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=9B=84=20=EC=82=AD=EC=A0=9C=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/umbba/api/controller/album/AlbumController.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java index 1d224103..d8eddaae 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/AlbumController.java @@ -69,11 +69,4 @@ public ApiResponse deleteAlbum(@PathVariable final Long albumId, final Principal public ApiResponse> getAlbumList(final Principal principal) { return ApiResponse.success(GET_ALBUM_LIST_SUCCESS, albumService.getAlbumList(getUserFromPrincial(principal))); } - - // 버킷에서 이미지 삭제 TODO 내부 로직으로 뺄 예정 - @DeleteMapping("/image") - public ApiResponse deleteImage(@RequestParam("img_url") String imgUrl) { - s3Service.deleteS3Image(imgUrl); - return ApiResponse.success(IMAGE_S3_DELETE_SUCCESS); - } } \ No newline at end of file From 674cf961b99eccf6b84be04cc253f1befe104337 Mon Sep 17 00:00:00 2001 From: jun02160 Date: Thu, 29 Feb 2024 00:07:46 +0900 Subject: [PATCH 10/11] =?UTF-8?q?[CHORE]=20=EC=9D=91=EB=8B=B5=EA=B0=92=20I?= =?UTF-8?q?D=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80,=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=20=EC=9D=91=EB=8B=B5=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9D=BC=EB=B6=80=20=EC=88=98=EC=A0=95=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/album/dto/response/AlbumResponseDto.java | 4 +++- .../java/sopt/org/umbba/common/exception/SuccessType.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/response/AlbumResponseDto.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/response/AlbumResponseDto.java index f56502c1..96789566 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/response/AlbumResponseDto.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/album/dto/response/AlbumResponseDto.java @@ -11,7 +11,8 @@ @Builder @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class AlbumResponseDto { - + + private Long albumId; private String title; private String content; private String writer; @@ -19,6 +20,7 @@ public class AlbumResponseDto { public static AlbumResponseDto of(Album album) { return AlbumResponseDto.builder() + .albumId(album.getId()) .title(album.getTitle()) .content(album.getContent()) .writer(album.getWriter()) diff --git a/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java b/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java index 6761ac19..25226397 100644 --- a/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java +++ b/umbba-common/src/main/java/sopt/org/umbba/common/exception/SuccessType.java @@ -41,7 +41,7 @@ public enum SuccessType { CREATE_PARENT_CHILD_SUCCESS(HttpStatus.CREATED, "온보딩 정보를 입력받아 부모자식 관계를 생성하는 데 성공했습니다."), MATCH_PARENT_CHILD_SUCCESS(HttpStatus.CREATED, "부모자식 관계 매칭에 성공했습니다."), ANSWER_TODAY_QUESTION_SUCCESS(HttpStatus.CREATED, "오늘의 일일문답에 답변을 완료하였습니다."), - CREATE_ALBUM_SUCCESS(HttpStatus.CREATED, "앨범의 기록 생성에 성공했습니다."), + CREATE_ALBUM_SUCCESS(HttpStatus.CREATED, "앨범의 기록 등록에 성공했습니다."), ; From 67a875df4930fe4aa13ee87047bf793132dd5f6d Mon Sep 17 00:00:00 2001 From: jun02160 Date: Thu, 29 Feb 2024 00:26:26 +0900 Subject: [PATCH 11/11] =?UTF-8?q?[CHORE]=20User=EC=9D=98=20Parentchild=20n?= =?UTF-8?q?ull=20=EC=97=AC=EB=B6=80=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umbba/api/service/album/AlbumService.java | 18 +++++++++++++++--- .../org/umbba/common/exception/ErrorType.java | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java b/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java index 5803c099..d9a1a261 100644 --- a/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java +++ b/umbba-api/src/main/java/sopt/org/umbba/api/service/album/AlbumService.java @@ -29,7 +29,8 @@ public class AlbumService { public Long createAlbum(final CreateAlbumRequestDto request, final String imgUrl, final Long userId) { User user = getUserById(userId); - Parentchild parentchild = user.getParentChild(); + Parentchild parentchild = getParentchildByUser(user); + Album album = Album.builder() .title(request.getTitle()) .content(request.getContent()) @@ -48,10 +49,11 @@ public Long createAlbum(final CreateAlbumRequestDto request, final String imgUrl public String deleteAlbum(final Long albumId, final Long userId) { User user = getUserById(userId); + Parentchild parentchild = getParentchildByUser(user); Album album = getAlbumById(albumId); album.deleteParentchild(); - user.getParentChild().deleteAlbum(album); + parentchild.deleteAlbum(album); albumRepository.delete(album); return album.getImgUrl(); @@ -59,8 +61,9 @@ public String deleteAlbum(final Long albumId, final Long userId) { public List getAlbumList(final Long userId) { User user = getUserById(userId); + Parentchild parentchild = getParentchildByUser(user); List albumList = albumRepository.findAllByParentchildOrderByCreatedAtDesc( - user.getParentChild()); + parentchild); return albumList.stream() .map(AlbumResponseDto::of) @@ -78,4 +81,13 @@ private Album getAlbumById(Long albumId) { () -> new CustomException(ErrorType.NOT_FOUND_ALBUM) ); } + + private Parentchild getParentchildByUser(User user) { + Parentchild parentchild = user.getParentChild(); + if (parentchild == null) { + throw new CustomException(ErrorType.USER_HAVE_NO_PARENTCHILD); + } + + return parentchild; + } } diff --git a/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java b/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java index 9e237040..3a170993 100644 --- a/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java +++ b/umbba-common/src/main/java/sopt/org/umbba/common/exception/ErrorType.java @@ -85,7 +85,7 @@ public enum ErrorType { FAIL_TO_SEND_PUSH_ALARM(HttpStatus.INTERNAL_SERVER_ERROR, "푸시 알림 메세지 전송에 실패했습니다."), FAIL_TO_GET_IMAGE_PRE_SIGNED_URL(HttpStatus.INTERNAL_SERVER_ERROR, "PreSigned Url을 가져오는 데 실패했습니다."), FAIL_TO_DELETE_IMAGE(HttpStatus.INTERNAL_SERVER_ERROR, "S3 버킷에서 이미지를 삭제하는 데 실패했습니다."), - S3_BUCKET_GET_IMAGE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "이미지를 불러오는 데 실패했습니다."), + S3_BUCKET_GET_IMAGE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 버킷에서 이미지를 불러오는 데 실패했습니다."), // ETC INDEX_OUT_OF_BOUNDS(HttpStatus.INTERNAL_SERVER_ERROR, "인덱스 범위를 초과했습니다."),