diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c078f26c..d9a7fb64 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,2 @@ -# BE 리뷰어 지정 -* @saokiritoni @floreo1242 @hysong4u -* @pkhyrn268 @llcc-ww \ No newline at end of file +# BE 리뷰어 지정 (be 1기 + mini be 파트) +* @DguFarmSystem/TF-BE-1st @pkhyrn268 @llcc-ww \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1544868c..5568a1e8 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/org/farmsystem/homepage/domain/blog/controller/docs/AdminBlogApi.java b/src/main/java/org/farmsystem/homepage/domain/blog/controller/docs/AdminBlogApi.java index 35f5177c..d7d64f5c 100644 --- a/src/main/java/org/farmsystem/homepage/domain/blog/controller/docs/AdminBlogApi.java +++ b/src/main/java/org/farmsystem/homepage/domain/blog/controller/docs/AdminBlogApi.java @@ -28,7 +28,7 @@ public interface AdminBlogApi { }) ResponseEntity> approveBlog( @Parameter(description = "승인할 블로그 ID") @PathVariable Long blogId, - @Parameter(description = "관리자 ID (인증 토큰)") @RequestParam Long userId + @Parameter(hidden = true) @RequestParam Long userId ); @Operation(summary = "블로그 거절", description = "승인 대기 중인 특정 블로그를 거절합니다.") @@ -41,7 +41,7 @@ ResponseEntity> approveBlog( }) ResponseEntity> rejectBlog( @Parameter(description = "거절할 블로그 ID") @PathVariable Long blogId, - @Parameter(description = "관리자 ID (인증 토큰)") @RequestParam Long userId + @Parameter(hidden = true) @RequestParam Long userId ); @Operation(summary = "승인 대기 중인 블로그 목록 조회", description = "관리자가 승인/거절할 블로그 목록을 조회합니다.") @@ -58,6 +58,6 @@ ResponseEntity> rejectBlog( }) ResponseEntity> createBlog( @RequestBody @Valid BlogRequestDTO request, - @Parameter(description = "관리자 ID (인증 토큰)") @RequestParam Long userId + @Parameter(hidden = true) Long userId ); } \ No newline at end of file diff --git a/src/main/java/org/farmsystem/homepage/domain/blog/entity/Blog.java b/src/main/java/org/farmsystem/homepage/domain/blog/entity/Blog.java index 707ecc72..4561558e 100644 --- a/src/main/java/org/farmsystem/homepage/domain/blog/entity/Blog.java +++ b/src/main/java/org/farmsystem/homepage/domain/blog/entity/Blog.java @@ -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; @@ -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 { @@ -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); diff --git a/src/main/java/org/farmsystem/homepage/domain/blog/service/AdminBlogService.java b/src/main/java/org/farmsystem/homepage/domain/blog/service/AdminBlogService.java index 9badef26..804b8e2d 100644 --- a/src/main/java/org/farmsystem/homepage/domain/blog/service/AdminBlogService.java +++ b/src/main/java/org/farmsystem/homepage/domain/blog/service/AdminBlogService.java @@ -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 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)); @@ -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)); @@ -52,37 +63,22 @@ public void rejectBlog(Long blogId, Long adminUserId) { blog.reject(admin); } - /** - * 승인 대기 중인 블로그 목록 조회 - */ - @Transactional(readOnly = true) - public List 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); diff --git a/src/main/java/org/farmsystem/homepage/domain/blog/service/BlogService.java b/src/main/java/org/farmsystem/homepage/domain/blog/service/BlogService.java index 690098bf..286198f4 100644 --- a/src/main/java/org/farmsystem/homepage/domain/blog/service/BlogService.java +++ b/src/main/java/org/farmsystem/homepage/domain/blog/service/BlogService.java @@ -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 getApprovedBlogs() { return blogRepository.findByApprovalStatus(ApprovalStatus.APPROVED).stream() .map(BlogResponseDTO::fromEntity) @@ -63,7 +41,6 @@ public List getApprovedBlogs() { /** * '내가 신청한 블로그' 목록 조회 */ - @Transactional(readOnly = true) public List getMyBlogs(Long userId) { return blogRepository.findByUser_UserId(userId).stream() .map(MyApplicationResponseDTO::fromEntity) @@ -73,10 +50,27 @@ public List getMyBlogs(Long userId) { /** * 최신 승인된 블로그 페이징 조회 */ - @Transactional(readOnly = true) public Page 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); + } } diff --git a/src/main/java/org/farmsystem/homepage/domain/common/service/S3Service.java b/src/main/java/org/farmsystem/homepage/domain/common/service/S3Service.java index 12521af5..e73cd325 100644 --- a/src/main/java/org/farmsystem/homepage/domain/common/service/S3Service.java +++ b/src/main/java/org/farmsystem/homepage/domain/common/service/S3Service.java @@ -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; @@ -19,7 +21,7 @@ @Slf4j public class S3Service { - private final AmazonS3 amazonS3; + private final S3Presigner s3Presigner; @Value("${cloud.aws.s3.bucket}") private String bucketName; @@ -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()); } // 파일 확장자 추출 diff --git a/src/main/java/org/farmsystem/homepage/domain/news/controller/AdminNewsApi.java b/src/main/java/org/farmsystem/homepage/domain/news/controller/AdminNewsApi.java index 71e7c3a9..ba34b466 100644 --- a/src/main/java/org/farmsystem/homepage/domain/news/controller/AdminNewsApi.java +++ b/src/main/java/org/farmsystem/homepage/domain/news/controller/AdminNewsApi.java @@ -24,7 +24,7 @@ public interface AdminNewsApi { description = "새로운 소식을 등록합니다.", security = @SecurityRequirement(name = "token") ) - + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse( responseCode = "201", @@ -43,6 +43,7 @@ public interface AdminNewsApi { description = "기존 소식 제목과 내용을 수정합니다.", security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse( responseCode = "200", @@ -64,6 +65,7 @@ ResponseEntity> updateNews( description = "ID를 통해 특정 소식을 삭제합니다.", security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse(responseCode = "204", description = "삭제 성공"), @ApiResponse(responseCode = "404", description = "존재하지 않는 소식 ID") diff --git a/src/main/java/org/farmsystem/homepage/domain/news/entity/News.java b/src/main/java/org/farmsystem/homepage/domain/news/entity/News.java index 7c90e95e..96560ba4 100644 --- a/src/main/java/org/farmsystem/homepage/domain/news/entity/News.java +++ b/src/main/java/org/farmsystem/homepage/domain/news/entity/News.java @@ -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 imageUrls; @ElementCollection diff --git a/src/main/java/org/farmsystem/homepage/domain/news/service/NewsService.java b/src/main/java/org/farmsystem/homepage/domain/news/service/NewsService.java index f395301d..76353608 100644 --- a/src/main/java/org/farmsystem/homepage/domain/news/service/NewsService.java +++ b/src/main/java/org/farmsystem/homepage/domain/news/service/NewsService.java @@ -16,6 +16,7 @@ @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class NewsService { private final NewsRepository newsRepository; @@ -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(), @@ -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)); diff --git a/src/main/java/org/farmsystem/homepage/domain/project/controller/AdminProjectApi.java b/src/main/java/org/farmsystem/homepage/domain/project/controller/AdminProjectApi.java index 8243e0b4..cdcf5a63 100644 --- a/src/main/java/org/farmsystem/homepage/domain/project/controller/AdminProjectApi.java +++ b/src/main/java/org/farmsystem/homepage/domain/project/controller/AdminProjectApi.java @@ -27,6 +27,7 @@ public interface AdminProjectApi { description = "관리자가 프로젝트를 승인합니다.", security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse( responseCode = "200", @@ -49,6 +50,7 @@ ResponseEntity> approveProject( description = "관리자가 프로젝트를 거절합니다.", security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse( responseCode = "200", @@ -71,6 +73,7 @@ ResponseEntity> rejectProject( description = "관리자가 아직 승인되지 않은 프로젝트들을 조회합니다.", security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse( responseCode = "200", diff --git a/src/main/java/org/farmsystem/homepage/domain/project/controller/ProjectApi.java b/src/main/java/org/farmsystem/homepage/domain/project/controller/ProjectApi.java index a97fed2f..06f09469 100644 --- a/src/main/java/org/farmsystem/homepage/domain/project/controller/ProjectApi.java +++ b/src/main/java/org/farmsystem/homepage/domain/project/controller/ProjectApi.java @@ -27,6 +27,7 @@ public interface ProjectApi { description = "유저가 프로젝트를 신청합니다.", security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse(responseCode = "204", description = "신청 성공") }) @@ -47,6 +48,7 @@ ResponseEntity> applyForProject( description = "유저가 신청한 프로젝트 목록을 조회합니다.", security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse( responseCode = "200", diff --git a/src/main/java/org/farmsystem/homepage/domain/user/controller/AdminUserApi.java b/src/main/java/org/farmsystem/homepage/domain/user/controller/AdminUserApi.java index 8942f010..f94f1a15 100644 --- a/src/main/java/org/farmsystem/homepage/domain/user/controller/AdminUserApi.java +++ b/src/main/java/org/farmsystem/homepage/domain/user/controller/AdminUserApi.java @@ -27,6 +27,7 @@ public interface AdminUserApi { "generation과 track 값 변경 시 변경 이력이 저장됩니다.", security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse(responseCode = "200", description = "사용자 정보 수정 성공", content = @Content( @@ -45,6 +46,7 @@ ResponseEntity> updateUser( description = "관리자가 사용자를 삭제하는 API입니다.", security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse(responseCode = "200", description = "사용자 삭제 성공"), @ApiResponse(responseCode = "404", description = "사용자 정보 없음") @@ -60,6 +62,7 @@ ResponseEntity> updateUser( "- 페이징 응답 값(pageResponseDTO 스키마 참고하기): pageSize(페이지당 데이터 개수), totalElements(총 데이터 개수), currentPageElements(현재 페이지 데이터 개수), totalPages(총 페이지 개수), currentPage(현재 페이지), sortBy(정렬 기준), hasNextPage(다음 페이지 존재 여부), hasPreviousPage(이전 페이지 존재 여부) \n" , security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse(responseCode = "200", description = "사용자 조회 성공", content = @Content( @@ -80,6 +83,7 @@ ResponseEntity> getAllUsers( "- 페이징 요청 옵션: page(페이지 번호), size(페이지 사이즈) + sort(정렬 기준) \n" , security = @SecurityRequirement(name = "token") ) + @SecurityRequirement(name = "JWT") @ApiResponses({ @ApiResponse(responseCode = "200", description = "삭제된 사용자 조회 성공", content = @Content( diff --git a/src/main/java/org/farmsystem/homepage/global/config/S3Config.java b/src/main/java/org/farmsystem/homepage/global/config/S3Config.java index 0d816d2f..1503e889 100644 --- a/src/main/java/org/farmsystem/homepage/global/config/S3Config.java +++ b/src/main/java/org/farmsystem/homepage/global/config/S3Config.java @@ -1,12 +1,13 @@ package org.farmsystem.homepage.global.config; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; 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.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +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 { @@ -17,19 +18,29 @@ public class S3Config { @Value("${cloud.aws.credentials.secretKey}") private String secretKey; - @Value("${cloud.aws.s3.bucket}") - private String bucketName; - @Value("${cloud.aws.region.static}") private String region; @Bean - public AmazonS3 amazonS3() { - BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + public S3Client s3Client() { + + AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey); + + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .build(); + } + + @Bean + public S3Presigner s3Presigner() { + + AwsBasicCredentials credentials = + AwsBasicCredentials.create(accessKey, secretKey); - return AmazonS3ClientBuilder.standard() - .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) - .withRegion(region) + return S3Presigner.builder() + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .region(Region.of(region)) .build(); } }