Skip to content
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
5 changes: 2 additions & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# BE 리뷰어 μ§€μ •
* @saokiritoni @floreo1242 @hysong4u
* @pkhyrn268 @llcc-ww
# BE 리뷰어 μ§€μ • (be 1κΈ° + mini be 파트)
* @DguFarmSystem/TF-BE-1st @pkhyrn268 @llcc-ww
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.0'

//aws
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.541'
implementation platform("software.amazon.awssdk:bom:2.25.0")
implementation "software.amazon.awssdk:s3"

//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public interface AdminBlogApi {
})
ResponseEntity<SuccessResponse<?>> approveBlog(
@Parameter(description = "μŠΉμΈν•  λΈ”λ‘œκ·Έ ID") @PathVariable Long blogId,
@Parameter(description = "κ΄€λ¦¬μž ID (인증 토큰)") @RequestParam Long userId
@Parameter(hidden = true) @RequestParam Long userId
);

@Operation(summary = "λΈ”λ‘œκ·Έ 거절", description = "승인 λŒ€κΈ° 쀑인 νŠΉμ • λΈ”λ‘œκ·Έλ₯Ό κ±°μ ˆν•©λ‹ˆλ‹€.")
Expand All @@ -41,7 +41,7 @@ ResponseEntity<SuccessResponse<?>> approveBlog(
})
ResponseEntity<SuccessResponse<?>> rejectBlog(
@Parameter(description = "κ±°μ ˆν•  λΈ”λ‘œκ·Έ ID") @PathVariable Long blogId,
@Parameter(description = "κ΄€λ¦¬μž ID (인증 토큰)") @RequestParam Long userId
@Parameter(hidden = true) @RequestParam Long userId
);

@Operation(summary = "승인 λŒ€κΈ° 쀑인 λΈ”λ‘œκ·Έ λͺ©λ‘ 쑰회", description = "κ΄€λ¦¬μžκ°€ 승인/κ±°μ ˆν•  λΈ”λ‘œκ·Έ λͺ©λ‘μ„ μ‘°νšŒν•©λ‹ˆλ‹€.")
Expand All @@ -58,6 +58,6 @@ ResponseEntity<SuccessResponse<?>> rejectBlog(
})
ResponseEntity<SuccessResponse<?>> createBlog(
@RequestBody @Valid BlogRequestDTO request,
@Parameter(description = "κ΄€λ¦¬μž ID (인증 토큰)") @RequestParam Long userId
@Parameter(hidden = true) Long userId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.persistence.*;
import lombok.*;
import org.farmsystem.homepage.domain.blog.dto.request.BlogRequestDTO;
import org.farmsystem.homepage.domain.common.entity.BaseTimeEntity;
import org.farmsystem.homepage.domain.user.entity.User;
import org.farmsystem.homepage.global.error.ErrorCode;
Expand All @@ -11,9 +12,7 @@
import java.util.Set;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "blog")
public class Blog extends BaseTimeEntity {
Expand All @@ -39,15 +38,38 @@ public class Blog extends BaseTimeEntity {
@Column(nullable = false)
private ApprovalStatus approvalStatus = ApprovalStatus.PENDING;

/**
* κ΄€λ¦¬μž κ΄€λ ¨ μ½”λ“œ
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "approved_by")
private User approvedBy;
private User approvedBy; // μˆ˜λ½ν•œ κ΄€λ¦¬μž

private LocalDateTime approvedAt;

// κ΄€λ¦¬μž 생성
private Blog(BlogRequestDTO request, User admin) {
this.link = request.link();
this.categories = request.categories();
this.user = admin;
this.approvalStatus = ApprovalStatus.APPROVED;
this.approvedBy = admin;
this.approvedAt = LocalDateTime.now();
}

// μ‚¬μš©μž μ‹ μ²­
private Blog(BlogRequestDTO request, User user, ApprovalStatus status) {
this.link = request.link();
this.categories = request.categories();
this.user = user;
this.approvalStatus = status; // PENDING μƒνƒœλ‘œ μ„€μ •
}

public static Blog createByAdmin(BlogRequestDTO request, User admin) {
return new Blog(request, admin);
}

public static Blog apply(BlogRequestDTO request, User user) {
return new Blog(request, user, ApprovalStatus.PENDING);
}

public void approve(User admin) {
if (this.approvalStatus != ApprovalStatus.PENDING) {
throw new BusinessException(ErrorCode.ALREADY_ACCEPTED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,26 @@
import static org.farmsystem.homepage.global.error.ErrorCode.*;

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

private final BlogRepository blogRepository;
private final UserRepository userRepository;

/**
* 승인 λŒ€κΈ° 쀑인 λΈ”λ‘œκ·Έ λͺ©λ‘ 쑰회
*/
public List<PendingBlogResponseDTO> getPendingBlogs() {
return blogRepository.findByApprovalStatus(ApprovalStatus.PENDING).stream()
.map(PendingBlogResponseDTO::fromEntity)
.toList();
}

/**
* λΈ”λ‘œκ·Έ 승인
*/
@Transactional
public BlogApprovalResponseDTO approveBlog(Long blogId, Long adminUserId) {
Blog blog = blogRepository.findById(blogId)
.orElseThrow(() -> new EntityNotFoundException(BLOG_NOT_FOUND));
Expand All @@ -42,6 +52,7 @@ public BlogApprovalResponseDTO approveBlog(Long blogId, Long adminUserId) {
/**
* λΈ”λ‘œκ·Έ 거절
*/
@Transactional
public void rejectBlog(Long blogId, Long adminUserId) {
Blog blog = blogRepository.findById(blogId)
.orElseThrow(() -> new EntityNotFoundException(BLOG_NOT_FOUND));
Expand All @@ -52,37 +63,22 @@ public void rejectBlog(Long blogId, Long adminUserId) {
blog.reject(admin);
}

/**
* 승인 λŒ€κΈ° 쀑인 λΈ”λ‘œκ·Έ λͺ©λ‘ 쑰회
*/
@Transactional(readOnly = true)
public List<PendingBlogResponseDTO> getPendingBlogs() {
return blogRepository.findByApprovalStatus(ApprovalStatus.PENDING).stream()
.map(PendingBlogResponseDTO::fromEntity)
.toList();
}

/**
* κ΄€λ¦¬μž - λΈ”λ‘œκ·Έ 직접 생성
* 승인 μƒνƒœλ₯Ό λ°”λ‘œ APPROVED둜 μ„€μ •ν•©λ‹ˆλ‹€.
*/
@Transactional
public BlogApprovalResponseDTO createBlog(BlogRequestDTO request, Long adminUserId) {
User admin = userRepository.findById(adminUserId)
.orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));

boolean exists = blogRepository.existsByUserAndLink(admin, request.link());
if (exists) {
throw new BusinessException(BLOG_DUPLICATED);
}

Blog blog = Blog.builder()
.link(request.link())
.user(admin)
.categories(request.categories())
.approvalStatus(ApprovalStatus.APPROVED)
.approvedBy(admin)
.approvedAt(java.time.LocalDateTime.now())
.build();

Blog blog = Blog.createByAdmin(request, admin);
blogRepository.save(blog);

return BlogApprovalResponseDTO.fromEntity(blog, admin);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,16 @@
import static org.farmsystem.homepage.global.error.ErrorCode.*;

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

private final BlogRepository blogRepository;
private final UserRepository userRepository;

/**
* λΈ”λ‘œκ·Έ μ‹ μ²­
*/
public void applyForBlog(BlogRequestDTO request, Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));

boolean exists = blogRepository.existsByUserAndLink(user, request.link());
if (exists) {
throw new BusinessException(BLOG_DUPLICATED);
}

Blog blog = Blog.builder()
.link(request.link())
.user(user)
.categories(request.categories())
.build();

blogRepository.save(blog);
}

/**
* 승인된 λΈ”λ‘œκ·Έ λͺ©λ‘ 쑰회
*/
@Transactional(readOnly = true)
public List<BlogResponseDTO> getApprovedBlogs() {
return blogRepository.findByApprovalStatus(ApprovalStatus.APPROVED).stream()
.map(BlogResponseDTO::fromEntity)
Expand All @@ -63,7 +41,6 @@ public List<BlogResponseDTO> getApprovedBlogs() {
/**
* 'λ‚΄κ°€ μ‹ μ²­ν•œ λΈ”λ‘œκ·Έ' λͺ©λ‘ 쑰회
*/
@Transactional(readOnly = true)
public List<MyApplicationResponseDTO> getMyBlogs(Long userId) {
return blogRepository.findByUser_UserId(userId).stream()
.map(MyApplicationResponseDTO::fromEntity)
Expand All @@ -73,10 +50,27 @@ public List<MyApplicationResponseDTO> getMyBlogs(Long userId) {
/**
* μ΅œμ‹  승인된 λΈ”λ‘œκ·Έ νŽ˜μ΄μ§• 쑰회
*/
@Transactional(readOnly = true)
public Page<BlogResponseDTO> getApprovedBlogsPaged(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "approvedAt"));
return blogRepository.findByApprovalStatus(ApprovalStatus.APPROVED, pageable)
.map(BlogResponseDTO::fromEntity);
}

/**
* λΈ”λ‘œκ·Έ μ‹ μ²­
*/
@Transactional
public void applyForBlog(BlogRequestDTO request, Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));

boolean exists = blogRepository.existsByUserAndLink(user, request.link());
if (exists) {
throw new BusinessException(BLOG_DUPLICATED);
}

Blog blog = Blog.apply(request, user);

blogRepository.save(blog);
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package org.farmsystem.homepage.domain.common.service;

import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.farmsystem.homepage.domain.common.dto.request.PresignedUrlRequestDTO;
import org.farmsystem.homepage.domain.common.dto.response.PresignedUrlResponseDTO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;

import java.net.URL;
import java.time.Duration;
import java.util.Arrays;
import java.util.Date;

Expand All @@ -19,7 +21,7 @@
@Slf4j
public class S3Service {

private final AmazonS3 amazonS3;
private final S3Presigner s3Presigner;

@Value("${cloud.aws.s3.bucket}")
private String bucketName;
Expand All @@ -38,17 +40,23 @@ public PresignedUrlResponseDTO generateProfilePresignedUrl(Long userId) {
}

// presigned url 생성
public PresignedUrlResponseDTO generatePresignedUrl(PresignedUrlRequestDTO presignedUrlRequest) {
String filePath = presignedUrlRequest.directory() + "/" + presignedUrlRequest.fileName();
Date expiration = new Date(System.currentTimeMillis() + 1000 * 60 * 15);
public PresignedUrlResponseDTO generatePresignedUrl(PresignedUrlRequestDTO request) {

String filePath = request.directory() + "/" + request.fileName();

GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, filePath)
.withMethod(HttpMethod.PUT)
.withExpiration(expiration);
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(filePath)
.contentType("image/png")
.build();

URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);
PresignedPutObjectRequest presignedRequest =
s3Presigner.presignPutObject(builder -> builder
.signatureDuration(Duration.ofMinutes(15))
.putObjectRequest(putObjectRequest)
);

return PresignedUrlResponseDTO.of(url.toString());
return PresignedUrlResponseDTO.of(presignedRequest.url().toString());
}

// 파일 ν™•μž₯자 μΆ”μΆœ
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public interface AdminNewsApi {
description = "μƒˆλ‘œμš΄ μ†Œμ‹μ„ λ“±λ‘ν•©λ‹ˆλ‹€.",
security = @SecurityRequirement(name = "token")
)

@SecurityRequirement(name = "JWT")
@ApiResponses({
@ApiResponse(
responseCode = "201",
Expand All @@ -43,6 +43,7 @@ public interface AdminNewsApi {
description = "κΈ°μ‘΄ μ†Œμ‹ 제λͺ©κ³Ό λ‚΄μš©μ„ μˆ˜μ •ν•©λ‹ˆλ‹€.",
security = @SecurityRequirement(name = "token")
)
@SecurityRequirement(name = "JWT")
@ApiResponses({
@ApiResponse(
responseCode = "200",
Expand All @@ -64,6 +65,7 @@ ResponseEntity<SuccessResponse<?>> updateNews(
description = "IDλ₯Ό 톡해 νŠΉμ • μ†Œμ‹μ„ μ‚­μ œν•©λ‹ˆλ‹€.",
security = @SecurityRequirement(name = "token")
)
@SecurityRequirement(name = "JWT")
@ApiResponses({
@ApiResponse(responseCode = "204", description = "μ‚­μ œ 성곡"),
@ApiResponse(responseCode = "404", description = "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ†Œμ‹ ID")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class News extends BaseTimeEntity {

@ElementCollection
@CollectionTable(name = "news_images", joinColumns = @JoinColumn(name = "news_id"))
@Column(name = "image_url")
@Column(name = "image_url", columnDefinition = "TEXT")
private List<String> imageUrls;

@ElementCollection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

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

private final NewsRepository newsRepository;
Expand All @@ -32,6 +33,7 @@ public NewsDetailResponseDTO getNewsById(Long newsId) {
return NewsDetailResponseDTO.fromEntity(news);
}

@Transactional
public NewsDetailResponseDTO createNews(NewsRequestDTO request) {
News news = new News(
request.title(),
Expand All @@ -44,6 +46,7 @@ public NewsDetailResponseDTO createNews(NewsRequestDTO request) {
return NewsDetailResponseDTO.fromEntity(news);
}

@Transactional
public NewsDetailResponseDTO updateNews(Long newsId, NewsRequestDTO request) {
News news = newsRepository.findById(newsId)
.orElseThrow(() -> new BusinessException(ErrorCode.NEWS_NOT_FOUND));
Expand Down
Loading
Loading