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

feat(#92): 이미지 삭제/조회 api 개발 및 리뷰 수정 로직 변경 #93

Merged
merged 3 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ sonar {
property 'sonar.language', 'java'
property 'sonar.sourceEncoding', 'UTF-8'
property("sonar.test.inclusions", "**/*Test.java")
property "sonar.exclusions", "**/test/**, **/*Application*.java, **/dto/**, **/entity/**, **/*Exception*.java, **/*RepositoryImpl.java, **/global/**, **/resources/**, **/*Dao*.java, **/dev/**"
property "sonar.exclusions", "**/test/**, **/*Application*.java, **/dto/**, **/entity/**, **/*Exception*.java, **/*RepositoryImpl.java, **/global/**, **/resources/**, **/*Dao*.java, **/dev/**, **/admin/**, **/Image*.java"
property "sonar.java.coveragePlugin", "jacoco"
property 'sonar.coverage.jacoco.xmlReportPaths', 'build/reports/jacoco/test/jacocoTestReport.xml'
}
Expand Down Expand Up @@ -127,6 +127,8 @@ jacocoTestReport {
"**/global/*",
"**/*Dao*",
"**/dev/**",
"**/admin/**",
"**/Image*"
])
}))
}
Expand Down Expand Up @@ -162,6 +164,8 @@ jacocoTestCoverageVerification {
"**/global/*",
"**/*Dao*",
"**/dev/**",
"**/admin/**",
"**/Image*"
])
}))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package everymeal.server.admin.controller;


import everymeal.server.admin.dto.AdminUserDto.DefaultProfileImageRes;
import everymeal.server.admin.service.AdminUserService;
import everymeal.server.global.dto.response.ApplicationResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/admin/users")
@RequiredArgsConstructor
@Tag(name = "Admin API", description = "어드민에서 관리되는 데이터 관련 API입니다.")
public class AdminController {

private final AdminUserService adminUserService;

@Operation(
summary = "유저의 기본 프로필 이미지 정보를 반환합니다.",
description =
"유저의 기본 프로필 이미지 정보를 반환합니다. <br/>"
+ "피그마에 노출되는 순서대로 반환합니다. <br/>"
+ "기본 이미지를 선택 시, imgUrl에 imageKey를 넣어주세요. <br/>")
@GetMapping("/default-profile-images")
public ApplicationResponse<List<DefaultProfileImageRes>> getDefaultProfileImages() {
return ApplicationResponse.ok(adminUserService.getDefaultProfileImages());
}
}
22 changes: 22 additions & 0 deletions src/main/java/everymeal/server/admin/dto/AdminUserDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package everymeal.server.admin.dto;

import static everymeal.server.global.util.aws.S3Util.getImgUrl;

import everymeal.server.admin.entity.UserDefaultProfileImage;
import io.swagger.v3.oas.annotations.media.Schema;

public class AdminUserDto {

@Schema(description = "어드민에서 관리되는 유저의 기본 프로필 이미지 정보를 담은 응답 DTO입니다.")
public record DefaultProfileImageRes(
@Schema(description = "유저의 기본 프로필 이미지의 idx입니다.") Long idx,
@Schema(description = "유저의 기본 프로필 이미지의 URL입니다.") String profileImageUrl,
@Schema(description = "유저의 기본 프로필 이미지의 imageKey입니다.") String imageKey) {
public static DefaultProfileImageRes of(UserDefaultProfileImage entity) {
return new DefaultProfileImageRes(
entity.getIdx(),
getImgUrl(entity.getProfileImgUrl()),
entity.getProfileImgUrl());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package everymeal.server.admin.entity;


import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;

@Getter
@Table(catalog = "admin", name = "user_default_profile_image")
@Entity
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
public class UserDefaultProfileImage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idx;

private String profileImgUrl;

@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
Comment on lines +26 to +28
Copy link
Member Author

Choose a reason for hiding this comment

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

@Qbeom0925 해당 데이터는 삭제여부로 관리하지 않고, 즉시 삭제를 하는 것이 추후 관리 포인트를 늘리지 않는 것이라 판단하여 meta 데이터로 생성일시만을 두었습니다.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package everymeal.server.admin.repository;


import everymeal.server.admin.entity.UserDefaultProfileImage;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserDefaultProfileImageRepository
extends JpaRepository<UserDefaultProfileImage, Long> {}
23 changes: 23 additions & 0 deletions src/main/java/everymeal/server/admin/service/AdminUserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package everymeal.server.admin.service;


import everymeal.server.admin.dto.AdminUserDto.DefaultProfileImageRes;
import everymeal.server.admin.repository.UserDefaultProfileImageRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
public class AdminUserService {

private final UserDefaultProfileImageRepository userDefaultProfileImageRepository;

public List<DefaultProfileImageRes> getDefaultProfileImages() {
return userDefaultProfileImageRepository.findAll().stream()
.map(DefaultProfileImageRes::of)
.toList();
}
}
13 changes: 13 additions & 0 deletions src/main/java/everymeal/server/global/util/aws/S3Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import java.io.File;
import java.net.URL;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class S3Util {

public static AmazonS3 amazonS3;
Expand Down Expand Up @@ -56,4 +59,14 @@ public static String getImgUrl(String fileName) {
URL url = amazonS3.getUrl(bucket, runningName + File.separator + fileName);
return url.toString();
}

public void deleteImage(String fileUrl) {
try {
String fileKey = "dev/" + fileUrl;
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileKey));
} catch (Exception e) {
e.printStackTrace();
log.error("S3 이미지 삭제 실패 fileUrl: {}", fileUrl);
}
Comment on lines +65 to +70
Copy link
Member Author

Choose a reason for hiding this comment

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

@Qbeom0925 S3에서 이미지 삭제 실패 시, 이유를 트랙킹하기 위해 로그를 남깁니다.
추가로 현재 S3에 prod 폴더가 없어서 dev로 하드코딩 해두었습니다.

Copy link
Member

Choose a reason for hiding this comment

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

확실히 실패에 대한 트래킹 목적의 로그를 남기는 것은 좋은 것 같습니다.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.net.URL;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand Down Expand Up @@ -50,4 +51,23 @@ public ApplicationResponse<S3GetResignedUrlRes> getPresignedUrl(
return ApplicationResponse.ok(
S3GetResignedUrlRes.builder().imageKey(fileName).url(test.toString()).build());
}

@Operation(
summary = "S3 이미지 삭제",
description =
"""
S3에 저장된 이미지를 삭제합니다.
언제 사용하는 API인가요?
1. 리뷰 작성 중 이미지를 업로드하고, 리뷰 작성을 취소할 때
2. 리뷰 수정 중 이미지를 업로드하고, 리뷰 수정을 취소할 때
3. 리뷰 삭제 시 이미지를 삭제할 때
4. 이미지를 잘못 업로드 했을 때
5. 유저 프로필 이미지를 변경할 때 ( 기본 이미지 제외 )
""")
@DeleteMapping("/image")
public ApplicationResponse<Void> deleteImage(
@RequestParam(value = "fileName") String fileName) {
s3Util.deleteImage(fileName);
return ApplicationResponse.ok();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.List;

public record ReviewCreateReq(
Expand All @@ -14,7 +15,7 @@ public record ReviewCreateReq(
description =
"학식에 대한 리뷰 평가 점수를 정수형 최소 1 ~ 최대 5점까지로 입력해주세요. 사진 리뷰인 경우 0으로 보내주세요.",
defaultValue = "5")
@Nullable
@NotNull
Integer grade,
@Schema(
description = "학식에 대한 리뷰 내용을 1글자 이상 입력해주세요. 사진 리뷰인 경우 null로 보내주세요.",
Comment on lines +18 to 21
Copy link
Member Author

Choose a reason for hiding this comment

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

nullable이면 NPE 발생해서 notNull로 수정했습니다.

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/everymeal/server/review/entity/Image.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ public Image(String imageUrl, Review review) {
this.isDeleted = false;
this.review = review;
}

public void deleteImage() {
this.isDeleted = true;
}
Comment on lines +38 to +41
Copy link
Member Author

Choose a reason for hiding this comment

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

이미지 객체에서 삭제 여부를 업데이트하는 로직이 ... 없더라구요?!@ 그래서 추가했습니다.

}
5 changes: 2 additions & 3 deletions src/main/java/everymeal/server/review/entity/Review.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,8 @@ public Review(
public void updateEntity(String content, int grade, List<Image> images, Boolean todayReview) {
this.content = content;
this.grade = grade;
if (images != null) {
this.images.clear();
}
// 이미지 연관 리스트 지우고 새로운 이미지 리스트로 교체
this.images.clear();
this.images = images;
this.isTodayReview = todayReview;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,16 @@ public ReviewPagingVOWithCnt getReview(ReviewDto.ReviewQueryParam queryParam) {
jpaQueryFactory
.select(review)
.from(review)
.leftJoin(review.images, image)
.leftJoin(review.reviewMarks, reviewMark)
.leftJoin(review.restaurant)
.on(restaurant.idx.eq(queryParam.restaurantIdx()))
.leftJoin(image)
.on(review.idx.eq(image.review.idx).and(image.isDeleted.eq(Boolean.FALSE)))
.leftJoin(reviewMark)
.on(review.idx.eq(reviewMark.review.idx))
.innerJoin(restaurant)
.on(
review.restaurant
.idx
.eq(restaurant.idx)
.and(restaurant.idx.eq(queryParam.restaurantIdx())))
.where(
gtReviewIdx(queryParam.cursorIdx()),
isDeleted(),
Expand All @@ -51,10 +57,22 @@ public ReviewPagingVOWithCnt getReview(ReviewDto.ReviewQueryParam queryParam) {
jpaQueryFactory
.select(review.idx.count())
.from(review)
.leftJoin(review.images, image)
.leftJoin(review.reviewMarks, reviewMark)
.leftJoin(review.restaurant)
.on(restaurant.idx.eq(queryParam.restaurantIdx()))
.leftJoin(image)
.on(
review.idx
.eq(image.review.idx)
.and(image.isDeleted.eq(Boolean.FALSE)))
.leftJoin(reviewMark)
.on(review.idx.eq(reviewMark.review.idx))
.innerJoin(restaurant)
.on(
review.restaurant
.idx
.eq(restaurant.idx)
.and(
restaurant.idx.eq(
queryParam
.restaurantIdx())))
.where(isDeleted(), eqToday(queryParam.filter()))
.fetchOne())
.intValue();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package everymeal.server.review.service;


import everymeal.server.review.entity.Image;
import everymeal.server.review.repository.ImageRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ImageCommServiceImpl {

private final ImageRepository imageRepository;

@Transactional
public void deleteImage(Image alreadyImg) {
imageRepository.delete(alreadyImg);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import static everymeal.server.global.exception.ExceptionList.REVIEW_MARK_NOT_FOUND;
import static everymeal.server.global.exception.ExceptionList.REVIEW_UNAUTHORIZED;
import static everymeal.server.global.exception.ExceptionList.STORE_NOT_FOUND;
import static everymeal.server.global.util.aws.S3Util.getImgUrl;

import everymeal.server.global.exception.ApplicationException;
import everymeal.server.global.util.TimeFormatUtil;
import everymeal.server.global.util.aws.S3Util;
import everymeal.server.meal.entity.Restaurant;
import everymeal.server.meal.service.RestaurantCommServiceImpl;
import everymeal.server.review.dto.request.ReviewCreateReq;
Expand Down Expand Up @@ -36,6 +38,8 @@ public class ReviewServiceImpl implements ReviewService {
private final UserCommServiceImpl userCommServiceImpl;
private final ReviewCommServiceImpl reviewCommServiceImpl;
private final StoreRepository storeRepository;
private final S3Util s3Util;
private final ImageCommServiceImpl imageCommServiceImpl;

@Override
@Transactional
Expand Down Expand Up @@ -80,6 +84,17 @@ public Long updateReview(ReviewCreateReq request, Long userIdx, Long reviewIdx)
Review review = reviewCommServiceImpl.getReviewEntity(reviewIdx);

// (2) 이미지 주소 <> 이미지 객체 치환
List<Image> alreadyImageList = review.getImages();
if (!alreadyImageList.isEmpty()) { // [1,2,3] <> [1,2,4] 3을 삭제
List<String> reqImgList = request.imageList();
for (Image alreadyImg : alreadyImageList) {
if (!reqImgList.contains(alreadyImg.getImageUrl())) {
s3Util.deleteImage(alreadyImg.getImageUrl());
imageCommServiceImpl.deleteImage(alreadyImg);
}
}
}

Comment on lines +87 to +97
Copy link
Member Author

Choose a reason for hiding this comment

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

원본 이미지와 수정하는 이미지를 비교해서,
수정하는 이미지 list에 원본 이미지가 없다면 삭제하도록 하는 로직을 추가했습니다.
s3에서 이미지 삭제까지가 하나의 트랜잭셔으로 잡혀서 디비와 스토리지 간의 데이터 일관성을 유지할 수 있도록 했습니다.

Copy link
Member

Choose a reason for hiding this comment

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

데이터 일관성까지 고려해서 만드신것이 너무 멋집니다!!

List<Image> imageList = getImageFromString(request.imageList());
User user = userCommServiceImpl.getUserEntity(userIdx);
if (review.getUser() != user) {
Expand All @@ -101,6 +116,7 @@ public Boolean deleteReview(Long userIdx, Long reviewIdx) {
Review review = reviewCommServiceImpl.getReviewEntity(reviewIdx, user);
// (2) 기존 데이터 삭제
review.getRestaurant().removeGrade(review.getGrade());
review.getImages().forEach(Image::deleteImage);
review.deleteEntity();

reviewCommServiceImpl
Expand All @@ -125,7 +141,7 @@ public ReviewGetRes getReviewWithNoOffSetPaging(ReviewDto.ReviewQueryParam query
vo.getIdx(),
vo.getRestaurant().getName(),
vo.getUser().getNickname(),
vo.getUser().getProfileImgUrl(),
getImgUrl(vo.getUser().getProfileImgUrl()),
vo.isTodayReview(),
vo.getGrade(),
vo.getContent(),
Expand Down
Loading