Skip to content

Commit

Permalink
Merge branch 'test' into feature/#23
Browse files Browse the repository at this point in the history
  • Loading branch information
sss4920 authored Jan 9, 2024
2 parents 44ce25f + ec1387b commit 1bd0c95
Show file tree
Hide file tree
Showing 13 changed files with 360 additions and 18 deletions.
9 changes: 9 additions & 0 deletions linkmind/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ dependencies {
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// S3 AWS
implementation("software.amazon.awssdk:bom:2.21.0")
implementation("software.amazon.awssdk:s3:2.21.0")
implementation 'org.apache.httpcomponents:httpclient:4.5.9'

// JSoup
implementation 'org.jsoup:jsoup:1.15.3'

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.app.toaster.common.advice;

import java.net.MalformedURLException;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
Expand Down Expand Up @@ -41,4 +43,9 @@ protected ResponseEntity<ApiResponse> handleConstraintDefinitionException(final
return ResponseEntity.status(e.getStatusCode())
.body(ApiResponse.error(Error.BAD_REQUEST_VALIDATION, fieldError.getDefaultMessage()));
}
@ExceptionHandler(MalformedURLException.class)
protected ApiResponse handleConstraintDefinitionException(final MalformedURLException e) {
return ApiResponse.error(Error.MALFORMED_URL_EXEPTION, Error.MALFORMED_URL_EXEPTION.getMessage());
}

}
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
package com.app.toaster.controller;

import java.io.IOException;

import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.app.toaster.common.dto.ApiResponse;
// import com.app.toaster.config.UserId;
import com.app.toaster.controller.request.toast.IsReadDto;
import com.app.toaster.controller.request.toast.OgRequestDto;
import com.app.toaster.controller.request.toast.SaveToastDto;
import com.app.toaster.controller.response.toast.IsReadResponse;
import com.app.toaster.exception.Success;
import com.app.toaster.service.parse.ParsingService;
import com.app.toaster.service.toast.ToastService;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
Expand All @@ -29,14 +35,26 @@
@Validated
public class ToastController {
private final ToastService toastService;
private final ParsingService parsingService;

@PostMapping("/og")
@ResponseStatus(HttpStatus.OK)
public ApiResponse getOgAdvanced(
@RequestBody OgRequestDto ogRequestDto
) throws IOException {
return ApiResponse.success(Success.PARSING_OG_SUCCESS, parsingService.getOg(ogRequestDto.linkUrl()));
}



@PostMapping("/save")
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse createToast(
@RequestHeader("userId") Long userId,
@RequestBody SaveToastDto requestDto
@RequestPart MultipartFile image,
SaveToastDto requestDto
) {
toastService.createToast(userId, requestDto);
toastService.createToast(userId, requestDto, image);
return ApiResponse.success(Success.CREATE_TOAST_SUCCESS);
}

Expand All @@ -54,9 +72,11 @@ public ApiResponse<IsReadResponse> updateIsRead(
public ApiResponse deleteToast( //나중에 softDelete로 변경
@RequestHeader("userId") Long userId,
@RequestParam Long toastId
) {
) throws IOException {
toastService.deleteToast(userId, toastId);
return ApiResponse.success(Success.DELETE_TOAST_SUCCESS);
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.app.toaster.controller.request.toast;

public record OgRequestDto(String linkUrl) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.app.toaster.controller.response.parse;

public record OgResponse(String titleAdvanced, String imageAdvanced) {
public static OgResponse of(String titleAdvanced, String imageAdvanced){
return new OgResponse(titleAdvanced, imageAdvanced);
}
}
9 changes: 7 additions & 2 deletions linkmind/src/main/java/com/app/toaster/domain/Toast.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ public class Toast {

private Boolean isRead;

private String thumbnailUrl;

@Builder
public Toast(User user, Category category, String title, String linkUrl) {
public Toast(User user, Category category, String title, String linkUrl, String thumbnailUrl) {
this.category = category;
this.user = user;
this.title = title;
this.linkUrl = linkUrl;
this.isRead = false;
this.thumbnailUrl = thumbnailUrl;
}

public void updateTitle(String title) {
Expand All @@ -62,6 +65,8 @@ public void updateIsRead(Boolean isRead) {

public void updateCategory(Category category){ this.category = category;}


public void updateThumbnail(String thumbnailUrl){
this.thumbnailUrl = thumbnailUrl;
}

}
6 changes: 6 additions & 0 deletions linkmind/src/main/java/com/app/toaster/exception/Error.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public enum Error {
NOT_FOUND_USER_EXCEPTION(HttpStatus.NOT_FOUND, "찾을 수 없는 유저입니다."),
NOT_FOUND_CATEGORY_EXCEPTION(HttpStatus.NOT_FOUND, "찾을 수 없는 카테고리 입니다."),
NOT_FOUND_TOAST_EXCEPTION(HttpStatus.NOT_FOUND, "찾을 수 없는 토스트 입니다."),
NOT_FOUND_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "s3 서비스에서 이미지를 찾을 수 없습니다."),
NOT_FOUND_TOAST_FILTER(HttpStatus.NOT_FOUND, "유효하지 않은 필터입니다."),
NOT_FOUND_TIMER(HttpStatus.NOT_FOUND, "찾을 수 없는 타이머입니다."),

Expand All @@ -25,6 +26,10 @@ public enum Error {
*/
BAD_REQUEST_ISREAD(HttpStatus.BAD_REQUEST, "isRead 값이 잘못요청 되었습니다."),
BAD_REQUEST_VALIDATION(HttpStatus.BAD_REQUEST, "유효한 값으로 요청을 다시 보내주세요."),
BAD_REQUEST_FILE_EXTENSION(HttpStatus.BAD_REQUEST, "파일형식이 잘못된 것 같습니다."),
BAD_REQUEST_FILE_SIZE(HttpStatus.BAD_REQUEST, "파일크기가 잘못된 것 같습니다. 최대 5MB"),
MALFORMED_URL_EXEPTION(HttpStatus.BAD_REQUEST, "url 링크가 잘못된 것 같습니다."),


/**
* 401 UNAUTHORIZED EXCEPTION
Expand All @@ -42,6 +47,7 @@ public enum Error {
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 서버 에러가 발생했습니다"),
INVALID_ENCRYPT_COMMUNICATION(HttpStatus.INTERNAL_SERVER_ERROR, "ios 통신 증명 과정 중 문제가 발생했습니다."),
CREATE_PUBLIC_KEY_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "publickey 생성 과정 중 문제가 발생했습니다."),
CREATE_TOAST_PROCCESS_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "토스트 저장 중 문제가 발생했습니다. 카테고리 또는 s3 관련 문제로 예상됩니다.")
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public enum Success {
DELETE_CATEGORY_SUCCESS(HttpStatus.OK, "카테고리 삭제 성공"),
DELETE_TIMER_SUCCESS(HttpStatus.OK, "타이머 삭제 성공"),
SEARCH_SUCCESS(HttpStatus.OK, "검색 성공"),
PARSING_OG_SUCCESS(HttpStatus.OK, "og 데이터 파싱 결과입니다. 크롤링을 막은 페이지는 기본이미지가 나옵니다."),


UPDATE_ISREAD_SUCCESS(HttpStatus.OK, "열람여부 수정 완료"),
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.app.toaster.external.client.aws;

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.SystemPropertyCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
public class AWSConfig {

private static final String AWS_ACCESS_KEY_ID = "aws.accessKeyId";
private static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey";

private final String accessKey;
private final String secretKey;
private final String regionString;

public AWSConfig(@Value("${aws-property.access-key}") final String accessKey,
@Value("${aws-property.secret-key}") final String secretKey,
@Value("${aws-property.aws-region}") final String regionString) {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.regionString = regionString;
}


@Bean
public SystemPropertyCredentialsProvider systemPropertyCredentialsProvider() {
System.setProperty(AWS_ACCESS_KEY_ID, accessKey);
System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey);
return SystemPropertyCredentialsProvider.create();
}

@Bean
public Region getRegion() {
return Region.of(regionString);
}

@Bean
public S3Client getS3Client() {
return S3Client.builder()
.region(getRegion())
.credentialsProvider(systemPropertyCredentialsProvider())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.app.toaster.external.client.aws;

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

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;


import com.app.toaster.exception.Error;
import com.app.toaster.exception.model.BadRequestException;
import com.app.toaster.exception.model.NotFoundException;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

@Component
public class S3Service {
private final String bucketName;
private final AWSConfig awsConfig;
private static final Long MAX_FILE_SIZE = 5 * 1024 * 1024L;


public S3Service(@Value("${aws-property.s3-bucket-name}") final String bucketName, AWSConfig awsConfig) {
this.bucketName = bucketName;
this.awsConfig = awsConfig;
}

public String uploadImage(MultipartFile multipartFile, String folder) {
final String key = folder + createFileName(multipartFile.getOriginalFilename());
final S3Client s3Client = awsConfig.getS3Client();

validateFileSize(multipartFile);

PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType(multipartFile.getContentType())
.contentLength(multipartFile.getSize())
.contentDisposition("inline")
.build();

try {
RequestBody requestBody = RequestBody.fromBytes(multipartFile.getBytes());
s3Client.putObject(request, requestBody);
return key;
} catch(IOException e) {
throw new NotFoundException(Error.NOT_FOUND_IMAGE_EXCEPTION, Error.NOT_FOUND_IMAGE_EXCEPTION.getMessage());
}
}

public List<String> uploadImages(List<MultipartFile> multipartFileList, String folder) {
final S3Client s3Client = awsConfig.getS3Client();
List<String> list = new ArrayList<>();
for (int i = 0; i < multipartFileList.size(); i++) {
String key = folder + createFileName(multipartFileList.get(i).getOriginalFilename());
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType(multipartFileList.get(i).getContentType())
.contentLength(multipartFileList.get(i).getSize())
.contentDisposition("inline")
.build();

try {
RequestBody requestBody = RequestBody.fromBytes(multipartFileList.get(i).getBytes());
s3Client.putObject(request, requestBody);
list.add(key);
} catch(IOException e) {
throw new NotFoundException(Error.NOT_FOUND_IMAGE_EXCEPTION, Error.NOT_FOUND_IMAGE_EXCEPTION.getMessage());
}
}
return list;
}

// 파일명 (중복 방지)
private String createFileName(String fileName) {
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}

// 파일 유효성 검사
private String getFileExtension(String fileName) {
if (fileName.length() == 0) {
throw new NotFoundException(Error.NOT_FOUND_IMAGE_EXCEPTION, Error.NOT_FOUND_IMAGE_EXCEPTION.getMessage());
}
ArrayList<String> fileValidate = new ArrayList<>();
fileValidate.add(".jpg");
fileValidate.add(".jpeg");
fileValidate.add(".png");
fileValidate.add(".JPG");
fileValidate.add(".JPEG");
fileValidate.add(".PNG");
String idxFileName = fileName.substring(fileName.lastIndexOf("."));
if (!fileValidate.contains(idxFileName)) {
throw new BadRequestException(Error.BAD_REQUEST_FILE_EXTENSION, Error.BAD_REQUEST_FILE_EXTENSION.getMessage());
}
return fileName.substring(fileName.lastIndexOf("."));
}

// 이미지 삭제
public void deleteImage(String key) throws IOException {
final S3Client s3Client = awsConfig.getS3Client();

s3Client.deleteObject((DeleteObjectRequest.Builder builder) ->
builder.bucket(bucketName)
.key(key)
.build()
);
}

private void validateFileSize(MultipartFile image) {
if (image.getSize() > MAX_FILE_SIZE) {
throw new BadRequestException(Error.BAD_REQUEST_FILE_SIZE, Error.BAD_REQUEST_FILE_SIZE.getMessage());
}
}
}
Loading

0 comments on commit 1bd0c95

Please sign in to comment.