Skip to content

Commit

Permalink
Merge pull request #75 from one-dream-us/dev
Browse files Browse the repository at this point in the history
뉴스사 조회 및 이미지 관리(S3) 기능 추가
  • Loading branch information
JuJaeng2 authored Feb 14, 2025
2 parents 48e0f02 + 27e036c commit 41c6240
Show file tree
Hide file tree
Showing 19 changed files with 369 additions and 33 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ dependencies {
implementation 'com.google.auth:google-auth-library-oauth2-http:1.19.0'
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'

// AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'


}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.onedreamus.project.global.config.s3;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
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;

@Configuration
public class S3Config {

@Value("${cloud.aws.credentials.access-key}")
private String accessKey;

@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;

@Value("${cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3Client amazonS3Client() {
AWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials))
.withRegion(region)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ public enum ErrorCode {
WRONG_ANSWER_NOTE_NOT_EXIST(HttpStatus.NOT_FOUND, "오답노트에 존재하지 않는 용어입니다."),

// Quiz
NOT_ENOUGH_DICTIONARY(HttpStatus.BAD_REQUEST, "핵심단어와 오답노트에 단어가 총 3개 이상 필요합니다.");
NOT_ENOUGH_DICTIONARY(HttpStatus.BAD_REQUEST, "핵심단어와 오답노트에 단어가 총 3개 이상 필요합니다."),

// S3
IMAGE_UPLOAD_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S3 이미지 업로드에 실패했습니다."),
AWS_SDK_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "AWS S3 SDK 에러가 발생하여 정보를 처리할 수 없습니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/onedreamus/project/global/s3/ImageCategory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.onedreamus.project.global.s3;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ImageCategory {

THUMBNAIL("thumbnail");

private final String name;
}
93 changes: 93 additions & 0 deletions src/main/java/com/onedreamus/project/global/s3/S3Uploader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.onedreamus.project.global.s3;

import com.amazonaws.SdkClientException;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.onedreamus.project.global.exception.ErrorCode;
import com.onedreamus.project.thisismoney.exception.S3Exception;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;


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

@Slf4j
@Component
@RequiredArgsConstructor
public class S3Uploader {

private final AmazonS3Client amazonS3Client;

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

/**
* <p>S3 이미지 업로드</p>
* S3에 이미지 업로드 후, 이미지 URL 반환
* @param multipartFile
* @param imageCategory
* @return
*/
public String uploadMultipartFileByStream(MultipartFile multipartFile, ImageCategory imageCategory) {

String filename = multipartFile.getOriginalFilename();
String newFilename = createFileName(filename, imageCategory);

ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(getContentType(filename));
metadata.setContentLength(multipartFile.getSize());

try {
amazonS3Client.putObject(
new PutObjectRequest(bucket, newFilename, multipartFile.getInputStream(), metadata));
} catch (IOException e) {
throw new S3Exception(ErrorCode.IMAGE_UPLOAD_FAIL);
}

return amazonS3Client.getUrl(bucket, newFilename).toString();
}

/**
* <p>S3 이미지 삭제</p>
* @param fileName
*/
public void deleteFile(String fileName) {
try {
amazonS3Client.deleteObject(bucket, fileName);
log.info(" S3 객체 삭제 : {}", fileName);
} catch (SdkClientException e) {
throw new S3Exception(ErrorCode.AWS_SDK_ERROR);
}
}

private String createFileName(String fileName, ImageCategory imageCategory) {
String random = UUID.randomUUID().toString();
return imageCategory.getName() + "-" + random + fileName;
}

private String getContentType(String filename) {
int idx = filename.lastIndexOf(".");
String extension = filename.substring(idx + 1);
return "image/" + extension;
}

private void deleteUploadedImageList(List<String> imageUrlList) {
for (String imageUrl : imageUrlList) {
deleteFile(getFileName(imageUrl));
}
}

@NotNull
public String getFileName(String imageUrl) {
return imageUrl.substring(imageUrl.lastIndexOf("/") + 1);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.onedreamus.project.thisismoney.controller;

import com.onedreamus.project.thisismoney.model.dto.*;
import com.onedreamus.project.thisismoney.service.AgencyService;
import com.onedreamus.project.thisismoney.service.NewsService;
import com.onedreamus.project.thisismoney.service.ScheduledNewsService;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -10,9 +11,11 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.util.List;

@RestController
@RequestMapping("/v1/back-office")
Expand All @@ -21,18 +24,19 @@ public class BackOfficeController {

private final ScheduledNewsService scheduledNewsService;
private final NewsService newsService;
private final AgencyService agencyService;

@PostMapping("/contents/news")
@Operation(summary = "뉴스 콘텐츠 즉시 업로드", description = "API가 호출되면 즉시 뉴스 콘텐츠 업로드 동작을 수행합니다.")
public ResponseEntity<String> uploadNews(@Valid @RequestBody NewsRequest newsRequest) {
public ResponseEntity<String> uploadNews(@Valid @ModelAttribute NewsRequest newsRequest) {
newsService.uploadNews(newsRequest);
return ResponseEntity.ok("콘텐츠 등록 완료");
}

@PostMapping("/contents/news/scheduled/{scheduledAt}")
@Operation(summary = "뉴스 콘텐츠 업로드 예약", description = "뉴스 콘텐츠 업로드 날짜를 설정하고 예약 합니다.")
public ResponseEntity<String> scheduleContentUpload(
@Valid @RequestBody NewsRequest newsRequest,
@Valid @ModelAttribute NewsRequest newsRequest,
@PathVariable("scheduledAt") LocalDate scheduledAt) {
scheduledNewsService.scheduleUploadNews(newsRequest, scheduledAt);
return ResponseEntity.ok("콘텐츠 등록 완료");
Expand Down Expand Up @@ -61,4 +65,11 @@ public ResponseEntity<NewsDetailResponse> getNewsDetail(@PathVariable("newsId")
NewsDetailResponse newsDetailResponse = newsService.getNewsDetail(newsId);
return ResponseEntity.ok(newsDetailResponse);
}

@GetMapping("/agency/{keyword}")
@Operation(summary = "뉴스사 검색", description = "keyword를 포함하는 모든 뉴스사를 조회합니다.")
public ResponseEntity<List<AgencySearch>> searchAgency(@PathVariable("keeyword") String keyword) {
List<AgencySearch> agencySearches = agencyService.searchAgency(keyword);
return ResponseEntity.ok(agencySearches);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.onedreamus.project.thisismoney.exception;

import com.onedreamus.project.global.exception.CustomException;
import com.onedreamus.project.global.exception.ErrorCode;

public class S3Exception extends CustomException {
public S3Exception(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,34 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.onedreamus.project.thisismoney.model.dto.NewsRequest;
import com.onedreamus.project.thisismoney.model.dto.ScheduledNewsRequest;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
public class NewsRequestConverter implements AttributeConverter<NewsRequest, String> {
public class NewsRequestConverter implements AttributeConverter<ScheduledNewsRequest, String> {

private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public String convertToDatabaseColumn(NewsRequest newsRequest) {
if (newsRequest == null) {
public String convertToDatabaseColumn(ScheduledNewsRequest scheduledNewsRequest) {
if (scheduledNewsRequest == null) {
return null;
}
try {
return objectMapper.writeValueAsString(newsRequest);
return objectMapper.writeValueAsString(scheduledNewsRequest);
} catch (Exception e) {
throw new IllegalArgumentException("Error converting NewsRequest to JSON string", e);
}
}

@Override
public NewsRequest convertToEntityAttribute(String dbData) {
public ScheduledNewsRequest convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
}
try {
return objectMapper.readValue(dbData, NewsRequest.class);
return objectMapper.readValue(dbData, ScheduledNewsRequest.class);
} catch (Exception e) {
throw new IllegalArgumentException("Error converting JSON string to NewsRequest", e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.onedreamus.project.thisismoney.model.dto;

import com.onedreamus.project.thisismoney.model.entity.Agency;
import lombok.*;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class AgencySearch {

private Integer id;
private String name;

public static AgencySearch from(Agency agency) {
return AgencySearch.builder()
.id(agency.getId())
.name(agency.getName())
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;

@AllArgsConstructor
@NoArgsConstructor
Expand All @@ -19,8 +20,8 @@ public class NewsRequest {
@NotBlank(message = "뉴스 제목은 필수 값 입니다.")
private String title; // 뉴스 제목

@NotBlank(message = "썸네일 URL은 필수 값 입니다.")
private String thumbnailUrl; // 썸네일 URL

private MultipartFile thumbnailImage; // 썸네일 URL

@NotBlank(message = "원본 링크는 필수 값 입니다.")
private String originalLink; // 기사 원본 링크
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.onedreamus.project.thisismoney.model.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class ScheduledNewsRequest {

private String title; // 뉴스 제목
private String thumbnailUrl; // 썸네일 URL
private String originalLink; // 기사 원본 링크
private String newsAgency; // 뉴스 업로드한 에이전시
private List<DictionarySentenceRequest> dictionarySentenceList;

public static ScheduledNewsRequest from(NewsRequest newsRequest, String thumbnailUrl) {
return ScheduledNewsRequest.builder()
.title(newsRequest.getTitle())
.thumbnailUrl(thumbnailUrl)
.originalLink(newsRequest.getOriginalLink())
.newsAgency(newsRequest.getNewsAgency())
.dictionarySentenceList(newsRequest.getDictionarySentenceList())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
public class ScheduledNewsResponse {

private Integer id;
private NewsRequest newsRequest;
private ScheduledNewsRequest newsRequest;
private LocalDate scheduledAt;

public static ScheduledNewsResponse from(ScheduledNews scheduledNews) {
return ScheduledNewsResponse.builder()
.id(scheduledNews.getId())
.newsRequest(scheduledNews.getNewsRequest())
.newsRequest(scheduledNews.getScheduledNewsRequest())
.scheduledAt(scheduledNews.getScheduledAt())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.onedreamus.project.thisismoney.model.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Builder
public class Agency {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

private String name;

public static Agency from(String name) {
return Agency.builder()
.name(name)
.build();
}

}
Loading

0 comments on commit 41c6240

Please sign in to comment.