Skip to content

Commit

Permalink
Merge pull request #25 from Leets-Official/feat/#23/이미지-도메인-개발
Browse files Browse the repository at this point in the history
[feat] #25 이미지 업로드 및 게시글과 함께 저장
  • Loading branch information
hyxklee authored Nov 10, 2024
2 parents de515d5 + 7307b45 commit a2b1b6c
Show file tree
Hide file tree
Showing 18 changed files with 332 additions and 35 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ dependencies {
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

implementation 'software.amazon.awssdk:s3:2.20.7'

}

tasks.named('test') {
Expand Down
15 changes: 13 additions & 2 deletions src/main/java/com/leets/X/domain/image/domain/Image.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.leets.X.domain.image.domain;

import com.leets.X.domain.image.dto.request.ImageDto;
import com.leets.X.domain.post.domain.Post;
import com.leets.X.global.common.domain.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.*;

Expand All @@ -9,7 +11,7 @@
@AllArgsConstructor
@Builder
@Getter
public class Image {
public class Image extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -20,8 +22,17 @@ public class Image {

private String url;

@ManyToOne( fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_Id")
private Post post;

public static Image from(ImageDto dto, Post post) {
return Image.builder()
.name(dto.name())
.url(dto.url())
.post(post)
.build();
}


}
11 changes: 11 additions & 0 deletions src/main/java/com/leets/X/domain/image/dto/request/ImageDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.leets.X.domain.image.dto.request;

public record ImageDto(
String name,
String url
) {
public static ImageDto of(String name, String url) {
return new ImageDto(name, url);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.leets.X.domain.image.dto.response;

import com.leets.X.domain.image.domain.Image;

public record ImageResponse(
Long id,
String name,
String url
) {
public static ImageResponse from(Image image) {
return new ImageResponse(image.getId(), image.getName(), image.getUrl());
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/leets/X/domain/image/exception/ErrorMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.leets.X.domain.image.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ErrorMessage {

S3_UPLOAD_FAIL(500, "이미지 업로드 중 에러가 발생했습니다.");

private final int code;
private final String message;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.leets.X.domain.image.exception;

import com.leets.X.global.common.exception.BaseException;

public class S3UploadException extends BaseException {
public S3UploadException() {
super(ErrorMessage.S3_UPLOAD_FAIL.getCode(), ErrorMessage.S3_UPLOAD_FAIL.getMessage());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.leets.X.domain.image.repository;

import com.leets.X.domain.image.domain.Image;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ImageRepository extends JpaRepository<Image, Long> {
}
33 changes: 33 additions & 0 deletions src/main/java/com/leets/X/domain/image/service/ImageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.leets.X.domain.image.service;

import com.leets.X.domain.image.domain.Image;
import com.leets.X.domain.image.dto.request.ImageDto;
import com.leets.X.domain.image.repository.ImageRepository;
import com.leets.X.domain.post.domain.Post;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ImageService {

private final ImageUploadService imageUploadService;
private final ImageRepository imageRepository;

@Transactional
public List<Image> save(List<MultipartFile> file, Post post) throws IOException {
List<ImageDto> dtoList = imageUploadService.uploadImages(file);

List<Image> imageList = dtoList.stream()
.map((ImageDto dto) -> Image.from(dto, post))
.toList();

return imageRepository.saveAll(imageList);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.leets.X.domain.image.service;

import com.leets.X.domain.image.dto.request.ImageDto;
import com.leets.X.domain.image.exception.S3UploadException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import software.amazon.awssdk.services.s3.model.S3Exception;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class ImageUploadService {
@Value("${aws.s3.bucketName}")
private String bucketName;
@Value("${aws.s3.region}")
private String region;

private final S3Client s3Client;

public List<ImageDto> uploadImages(List<MultipartFile> files) throws IOException {

List<ImageDto> images = new ArrayList<>();

for (MultipartFile file : files) {
String originalName = file.getOriginalFilename();
String fileName = generateFileName(originalName);

try {
// PutObjectRequest 생성 및 설정
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(fileName)
.contentType(file.getContentType())
.build();

// S3에 파일 업로드
PutObjectResponse response = s3Client.putObject(
putObjectRequest,
RequestBody.fromInputStream(file.getInputStream(), file.getSize())
);

// 업로드 성공 여부 확인
if (response.sdkHttpResponse().isSuccessful()) {
// 업로드된 파일의 URL을 ImageDto로 추가
images.add(ImageDto.of(originalName, generateFileUrl(fileName)));
} else {
throw new S3UploadException();
}
} catch (S3Exception e) {
throw new S3UploadException();
}
}

return images;
}
// S3에 저장된 파일 URL 생성
private String generateFileUrl(String fileName) {
return String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, fileName);
}

// 파일 이름을 고유하게 생성하는 메서드
private String generateFileName(String originalFileName) {
String uuid = UUID.randomUUID().toString();
return uuid + "_" + originalFileName.replaceAll("\\s+", "_");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;


//컨트롤러에서 ResponseDto만들게끔
@Tag(name = "POST")
@RestController
@RequestMapping("/api/v1/posts")
Expand Down Expand Up @@ -55,11 +60,13 @@ public ResponseDto<List<ParentPostResponseDto>> getLatestPosts(@AuthenticationPr
}


@PostMapping("/post")
@PostMapping(value = "/post", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "글 생성")
public ResponseDto<PostResponseDto> createPost(@RequestBody PostRequestDTO postRequestDTO, @AuthenticationPrincipal String email) {
public ResponseDto<PostResponseDto> createPost(@RequestPart PostRequestDTO postRequestDTO,
@RequestPart(value = "files", required = false) List<MultipartFile> files,
@AuthenticationPrincipal String email) throws IOException {
// 인증된 사용자의 이메일을 `@AuthenticationPrincipal`을 통해 주입받음
PostResponseDto postResponseDto = postService.createPost(postRequestDTO, email);
PostResponseDto postResponseDto = postService.createPost(postRequestDTO, files , email);
return ResponseDto.response(ResponseMessage.POST_SUCCESS.getCode(), ResponseMessage.POST_SUCCESS.getMessage(), postResponseDto);
}

Expand All @@ -70,11 +77,14 @@ public ResponseDto<String> addLike(@PathVariable Long postId, @AuthenticationPri
return ResponseDto.response(ResponseMessage.ADD_LIKE_SUCCESS.getCode(), responseMessage);
}

@PostMapping("/{postId}/reply")
@PostMapping(value = "/{postId}/reply", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "답글 생성")
public ResponseDto<PostResponseDto> createReply(@PathVariable Long postId, @RequestBody PostRequestDTO postRequestDTO, @AuthenticationPrincipal String email) {
public ResponseDto<PostResponseDto> createReply(@PathVariable Long postId,
@RequestPart PostRequestDTO postRequestDTO,
@RequestPart(value = "files", required = false) List<MultipartFile> files,
@AuthenticationPrincipal String email) throws IOException {
// 답글 생성 서비스 호출 (부모 ID를 직접 전달)
PostResponseDto postResponseDto = postService.createReply(postId, postRequestDTO, email);
PostResponseDto postResponseDto = postService.createReply(postId, postRequestDTO, files, email);
return ResponseDto.response(ResponseMessage.REPLY_SUCCESS.getCode(), ResponseMessage.REPLY_SUCCESS.getMessage(), postResponseDto);
}

Expand All @@ -94,6 +104,4 @@ public ResponseDto<String> cancelLike(@PathVariable Long postId, @Authentication
return ResponseDto.response(ResponseMessage.LIKE_CANCEL_SUCCESS.getCode(), responseMessage);
}



}
17 changes: 15 additions & 2 deletions src/main/java/com/leets/X/domain/post/domain/Post.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.leets.X.domain.post.domain;


import com.leets.X.domain.image.domain.Image;
import com.leets.X.domain.like.domain.Like;
import com.leets.X.domain.post.domain.enums.IsDeleted;
Expand Down Expand Up @@ -40,7 +39,7 @@ public class Post extends BaseTimeEntity {
@OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Like> likes = new ArrayList<>();

@OneToMany(mappedBy = "post", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, fetch = FetchType.LAZY)
@OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Image> images = new ArrayList<>();


Expand All @@ -54,19 +53,28 @@ public class Post extends BaseTimeEntity {
private LocalDateTime deletedAt;

// 좋아요 수를 관리하기 위한 필드

@Column(name = "like_count")
private Long likeCount = 0L; // 기본값을 0L로 초기화하여 null을 방지

public void updateLikeCount(long newLikeCount) {
this.likeCount = newLikeCount;
}

public void decrementLikeCount() {
if (this.likeCount == null || this.likeCount == 0) {
this.likeCount = 0L; // null이거나 0일 경우 0으로 유지
} else {
this.likeCount--;
}
}

public long getLikesCount() {
return likeCount != null ? likeCount : 0; // null일 경우 0 반환
}



// 서비스에서 글의 상태를 삭제 상태로 바꾸기 위한 메서드
public void delete() {
if (this.isDeleted == IsDeleted.ACTIVE) { // 이미 삭제 상태가 아닐 때만 변경
Expand Down Expand Up @@ -95,8 +103,13 @@ public static Post create(User user, String content, Post parent) {
.likeCount(0L) // 기본값 설정
.isDeleted(IsDeleted.ACTIVE) // 기본값 설정
.parent(parent) // 부모 글 설정
.images(new ArrayList<>()) // 빈 리스트로 초기화
.build();

}

public void addImage(List<Image> images) {
this.images.addAll(images); // 기존 리스트에 이미지 추가
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ public record ParentPostResponseDto(
IsDeleted isDeleted,
LocalDateTime createdAt,
PostUserResponse user,
List<ImageResponseDto> images,
Long likeCount,
Boolean isLikedByUser
Boolean isLikedByUser,
List<ImageResponseDto> images
) {
public static ParentPostResponseDto from(Post post, boolean isLikedByUser) {
return new ParentPostResponseDto(
Expand All @@ -28,10 +28,10 @@ public static ParentPostResponseDto from(Post post, boolean isLikedByUser) {
post.getIsDeleted(),
post.getCreatedAt(),
convertUser(post.getUser()), // User 변환
convertImagesToDtoList(post), // Images 변환
post.getLikesCount(),
isLikedByUser // 좋아요 여부 설정
);
isLikedByUser, // 좋아요 여부 설정
convertImagesToDtoList(post) // Images 변환
);
}

private static PostUserResponse convertUser(User user) {
Expand Down
Loading

0 comments on commit a2b1b6c

Please sign in to comment.