Skip to content

Commit

Permalink
Merge pull request #150 from TrainingDiary/refactor/image-diet
Browse files Browse the repository at this point in the history
ImageUtil 클래스를 S3 역할 따로 구분 및 식단 서비스 컨트롤러 리팩토링
  • Loading branch information
kyoo0115 authored Jul 26, 2024
2 parents 099f763 + 6d15928 commit 2e3b549
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 165 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class DietController {
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "415", description = "유효하지 않은 미디어 타입 입니다.")
})
@PreAuthorize("hasRole('TRAINEE')")
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Expand All @@ -65,6 +66,7 @@ public ResponseEntity<Void> createDiet(
@GetMapping("{id}")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "403", description = "트레이너랑 트레이니 사이의 계약이 없습니다.")
})
@PreAuthorize("hasRole('TRAINER') or hasRole('TRAINEE')")
public ResponseEntity<Page<DietImageResponseDto>> getTraineeDiets(
Expand All @@ -84,6 +86,7 @@ public ResponseEntity<Page<DietImageResponseDto>> getTraineeDiets(
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "403", description = "트레이너랑 트레이니 사이의 계약이 없습니다.")
})
@PreAuthorize("hasRole('TRAINER') or hasRole('TRAINEE')")
@GetMapping("{id}/details")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.project.trainingdiary.dto.response.comment.CommentDto;
import com.project.trainingdiary.entity.CommentEntity;
import com.project.trainingdiary.entity.DietEntity;
import com.project.trainingdiary.util.ConvertCloudFrontUrlUtil;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;
Expand All @@ -18,20 +19,19 @@ public class DietDetailsInfoResponseDto {
private List<CommentDto> comments;
private LocalDate createdDate;

public static DietDetailsInfoResponseDto of(DietEntity diet,
List<CommentEntity> comments) {
public static DietDetailsInfoResponseDto of(DietEntity diet, List<CommentEntity> comments) {

List<CommentDto> commentDto = comments.stream()
.sorted((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt()))
.map(CommentDto::fromEntity)
List<CommentDto> commentDtoList = comments.stream()
.sorted((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt())) // 최신 댓글이 위로 오도록 정렬
.map(CommentDto::fromEntity) // CommentEntity를 CommentDto로 변환
.toList();

return DietDetailsInfoResponseDto.builder()
.id(diet.getId())
.imageUrl(diet.getOriginalUrl())
.imageUrl(ConvertCloudFrontUrlUtil.convertToCloudFrontUrl(diet.getOriginalUrl()))
.content(diet.getContent())
.comments(commentDto)
.comments(commentDtoList)
.createdDate(diet.getCreatedAt().toLocalDate())
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,22 @@ private static int calculateAge(LocalDate birthDate) {
return birthDate != null ? Period.between(birthDate, LocalDate.now()).getYears() : 0;
}

private static List<WeightHistoryDto> mapToWeightHistory(List<InBodyRecordHistoryEntity> inBodyRecords) {
private static List<WeightHistoryDto> mapToWeightHistory(
List<InBodyRecordHistoryEntity> inBodyRecords) {
return inBodyRecords.stream()
.map(WeightHistoryDto::fromEntity)
.collect(Collectors.toList());
}

private static List<BodyFatHistoryDto> mapToBodyFatHistory(List<InBodyRecordHistoryEntity> inBodyRecords) {
private static List<BodyFatHistoryDto> mapToBodyFatHistory(
List<InBodyRecordHistoryEntity> inBodyRecords) {
return inBodyRecords.stream()
.map(BodyFatHistoryDto::fromEntity)
.collect(Collectors.toList());
}

private static List<MuscleMassHistoryDto> mapToMuscleMassHistory(List<InBodyRecordHistoryEntity> inBodyRecords) {
private static List<MuscleMassHistoryDto> mapToMuscleMassHistory(
List<InBodyRecordHistoryEntity> inBodyRecords) {
return inBodyRecords.stream()
.map(MuscleMassHistoryDto::fromEntity)
.collect(Collectors.toList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
public class PtContractNotExistException extends GlobalException {

public PtContractNotExistException() {
super(HttpStatus.NOT_FOUND, "계약이 없습니다.");
super(HttpStatus.FORBIDDEN, "계약이 없습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.project.trainingdiary.provider;

import static com.project.trainingdiary.util.MediaUtil.getExtension;
import static com.project.trainingdiary.util.MediaUtil.getMediaType;

import com.project.trainingdiary.util.MediaUtil;
import io.awspring.cloud.s3.ObjectMetadata;
import io.awspring.cloud.s3.S3Operations;
import io.awspring.cloud.s3.S3Resource;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
import javax.imageio.ImageIO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

@RequiredArgsConstructor
@Component
public class S3DietImageProvider {

@Value("${spring.cloud.aws.s3.bucket}")
private String bucket;

private final S3Operations s3Operations;

public String uploadImageToS3(MultipartFile file) throws IOException {
BufferedImage originalImage = ImageIO.read(file.getInputStream());
BufferedImage resizedImage = MediaUtil.resizeOriginalImage(originalImage);
String extension = getExtension(MediaUtil.checkFileNameExist(file));
String key = UUID.randomUUID() + "." + extension;

try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(resizedImage, extension, baos);
try (InputStream inputStream = new ByteArrayInputStream(baos.toByteArray())) {
S3Resource s3Resource = s3Operations.upload(bucket, key, inputStream,
ObjectMetadata.builder().contentType(file.getContentType()).build());
return s3Resource.getURL().toExternalForm();
}
}
}

public String uploadThumbnailToS3(MultipartFile file, String originalKey, String extension)
throws IOException {
BufferedImage originalImage = ImageIO.read(file.getInputStream());
BufferedImage thumbnailImage = MediaUtil.resizeThumbnail(originalImage);

try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
ImageIO.write(thumbnailImage, extension, byteArrayOutputStream);
try (InputStream inputStream = new ByteArrayInputStream(
byteArrayOutputStream.toByteArray())) {
String thumbnailKey = "thumb_" + originalKey.substring(originalKey.lastIndexOf("/") + 1);
S3Resource s3Resource = s3Operations.upload(bucket, thumbnailKey, inputStream,
ObjectMetadata.builder().contentType(getMediaType(extension)).build());
return s3Resource.getURL().toExternalForm();
}
}
}

public void deleteFileFromS3(String fileUrl) {
String fileKey = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
s3Operations.deleteObject(bucket, fileKey);
}
}

This file was deleted.

107 changes: 69 additions & 38 deletions src/main/java/com/project/trainingdiary/service/DietService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
import com.project.trainingdiary.exception.user.UserNotFoundException;
import com.project.trainingdiary.exception.workout.InvalidFileTypeException;
import com.project.trainingdiary.model.type.UserRoleType;
import com.project.trainingdiary.provider.S3DietImageProvider;
import com.project.trainingdiary.repository.DietRepository;
import com.project.trainingdiary.repository.TraineeRepository;
import com.project.trainingdiary.repository.TrainerRepository;
import com.project.trainingdiary.repository.ptContract.PtContractRepository;
import com.project.trainingdiary.util.ImageUtil;
import com.project.trainingdiary.util.ConvertCloudFrontUrlUtil;
import com.project.trainingdiary.util.MediaUtil;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -39,7 +41,7 @@ public class DietService {
private final TrainerRepository trainerRepository;
private final PtContractRepository ptContractRepository;

private final ImageUtil imageUtil;
private final S3DietImageProvider s3DietImageProvider;

/**
* 새로운 식단을 생성합니다.
Expand All @@ -53,13 +55,12 @@ public void createDiet(CreateDietRequestDto dto) throws IOException {
TraineeEntity trainee = getAuthenticatedTrainee();
MultipartFile imageFile = dto.getImage();

if (!imageUtil.isValidImageType(imageFile)) {
throw new InvalidFileTypeException();
}
validateImageFileType(imageFile);

String originalUrl = imageUtil.uploadImageToS3(imageFile);
String extension = imageUtil.getExtension(imageFile.getOriginalFilename());
String thumbnailUrl = imageUtil.createAndUploadThumbnail(imageFile, originalUrl, extension);
String originalUrl = s3DietImageProvider.uploadImageToS3(imageFile);
String extension = MediaUtil.getExtension(MediaUtil.checkFileNameExist(imageFile));
String thumbnailUrl = s3DietImageProvider.uploadThumbnailToS3(imageFile, originalUrl,
extension);

DietEntity diet = new DietEntity();
diet.setTrainee(trainee);
Expand Down Expand Up @@ -99,14 +100,9 @@ public Page<DietImageResponseDto> getDiets(Long id, Pageable pageable) {
private Page<DietImageResponseDto> getDietsForTraineeByTrainer(Long id, Pageable pageable) {
TrainerEntity trainer = getAuthenticatedTrainer();
TraineeEntity trainee = getTraineeById(id);
hasContractWithTrainee(trainer, trainee);
validateContractWithTrainee(trainer, trainee);

Page<DietEntity> dietPage = dietRepository.findByTraineeId(id, pageable);

return dietPage.map(diet -> DietImageResponseDto.builder()
.dietId(diet.getId())
.thumbnailUrl(diet.getThumbnailUrl())
.build());
return mapToDietImageResponseDtos(dietRepository.findByTraineeId(id, pageable));
}

/**
Expand All @@ -123,12 +119,7 @@ private Page<DietImageResponseDto> getDietsForTrainee(Long id, Pageable pageable
throw new DietNotExistException();
}

Page<DietEntity> dietPage = dietRepository.findByTraineeId(id, pageable);

return dietPage.map(diet -> DietImageResponseDto.builder()
.dietId(diet.getId())
.thumbnailUrl(diet.getThumbnailUrl())
.build());
return mapToDietImageResponseDtos(dietRepository.findByTraineeId(id, pageable));
}

/**
Expand Down Expand Up @@ -177,7 +168,7 @@ private DietDetailsInfoResponseDto getDietDetailsInfoForTrainer(Long id) {
DietEntity diet = dietRepository.findByIdWithCommentsAndTrainer(id)
.orElseThrow(DietNotExistException::new);

hasContractWithTrainee(trainer, diet.getTrainee());
validateContractWithTrainee(trainer, diet.getTrainee());

return DietDetailsInfoResponseDto.of(diet, diet.getComments());
}
Expand All @@ -190,14 +181,12 @@ private DietDetailsInfoResponseDto getDietDetailsInfoForTrainer(Long id) {
*/
@Transactional
public void deleteDiet(Long id) {

TraineeEntity trainee = getAuthenticatedTrainee();

DietEntity diet = dietRepository.findByTraineeIdAndId(trainee.getId(), id)
.orElseThrow(DietNotExistException::new);

imageUtil.deleteFileFromS3(diet.getOriginalUrl());
imageUtil.deleteFileFromS3(diet.getThumbnailUrl());
deleteDietImages(diet);

dietRepository.delete(diet);
}
Expand All @@ -209,7 +198,7 @@ public void deleteDiet(Long id) {
* @param trainee 트레이니 엔티티
* @throws PtContractNotExistException 트레이너와 트레이니 사이에 계약이 없을 경우 예외 발생
*/
private void hasContractWithTrainee(TrainerEntity trainer, TraineeEntity trainee) {
private void validateContractWithTrainee(TrainerEntity trainer, TraineeEntity trainee) {
ptContractRepository.findByTrainerIdAndTraineeId(trainer.getId(), trainee.getId())
.orElseThrow(PtContractNotExistException::new);
}
Expand All @@ -221,10 +210,7 @@ private void hasContractWithTrainee(TrainerEntity trainer, TraineeEntity trainee
* @throws TraineeNotFoundException 인증된 트레이니가 존재하지 않을 경우 예외 발생
*/
private TraineeEntity getAuthenticatedTrainee() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getName() == null) {
throw new TraineeNotFoundException();
}
Authentication authentication = getAuthentication();
String email = authentication.getName();
return traineeRepository.findByEmail(email)
.orElseThrow(TraineeNotFoundException::new);
Expand All @@ -237,15 +223,13 @@ private TraineeEntity getAuthenticatedTrainee() {
* @throws TrainerNotFoundException 인증된 트레이너가 존재하지 않을 경우 예외 발생
*/
private TrainerEntity getAuthenticatedTrainer() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getName() == null) {
throw new TrainerNotFoundException();
}
Authentication authentication = getAuthentication();
String email = authentication.getName();
return trainerRepository.findByEmail(email)
.orElseThrow(TrainerNotFoundException::new);
}


/**
* 트레이니 ID로 트레이니를 조회합니다.
*
Expand All @@ -265,10 +249,57 @@ private TraineeEntity getTraineeById(Long id) {
*/
private UserRoleType getMyRole() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_TRAINER"))) {
return UserRoleType.TRAINER;
} else {
return UserRoleType.TRAINEE;
return auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_TRAINER")) ? UserRoleType.TRAINER
: UserRoleType.TRAINEE;
}

/**
* 이미지 파일 타입을 확인합니다.
*
* @param imageFile 이미지 파일
* @throws InvalidFileTypeException 이미지 파일 타입이 유효하지 않을 경우 예외 발생
*/
private void validateImageFileType(MultipartFile imageFile) {
if (!MediaUtil.isValidImageType(imageFile)) {
throw new InvalidFileTypeException();
}
}

/**
* 다이어트 이미지를 삭제합니다.
*
* @param diet 다이어트 엔티티
*/
private void deleteDietImages(DietEntity diet) {
s3DietImageProvider.deleteFileFromS3(diet.getOriginalUrl());
s3DietImageProvider.deleteFileFromS3(diet.getThumbnailUrl());
}

/**
* Authentication 객체를 반환합니다.
*
* @return 인증된 Authentication 객체
* @throws TraineeNotFoundException 인증된 트레이니가 존재하지 않을 경우 예외 발생
*/
private Authentication getAuthentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getName() == null) {
throw new TraineeNotFoundException();
}
return authentication;
}

/**
* 다이어트 엔티티 페이지를 다이어트 이미지 응답 DTO 페이지로 변환합니다.
*
* @param dietPage 다이어트 엔티티 페이지
* @return 다이어트 이미지 응답 DTO 페이지
*/
private Page<DietImageResponseDto> mapToDietImageResponseDtos(Page<DietEntity> dietPage) {
return dietPage.map(diet -> DietImageResponseDto.builder()
.dietId(diet.getId())
.thumbnailUrl(ConvertCloudFrontUrlUtil.convertToCloudFrontUrl(diet.getThumbnailUrl()))
.build());
}
}
Loading

0 comments on commit 2e3b549

Please sign in to comment.