Skip to content

Commit

Permalink
Merge pull request #81 from reading-log/develop
Browse files Browse the repository at this point in the history
04.03 Main merge
  • Loading branch information
don9m1n authored Apr 3, 2024
2 parents 41f9016 + 920383a commit 7f9e870
Show file tree
Hide file tree
Showing 52 changed files with 1,358 additions and 247 deletions.
13 changes: 13 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

/* Email */
implementation 'org.springframework.boot:spring-boot-starter-mail'

/* Thymeleaf */
implementation'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'

/* Redis */
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

/* Swagger */
implementation group: 'io.swagger.core.v3', name: 'swagger-core-jakarta', version: '2.2.7'
}

tasks.named('bootBuildImage') {
Expand Down
64 changes: 29 additions & 35 deletions src/main/java/com/api/readinglog/common/aws/AmazonS3Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.api.readinglog.common.exception.ErrorCode;
import com.api.readinglog.common.exception.custom.AwsS3Exception;
import com.api.readinglog.common.image.ImageUtil;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -26,61 +29,52 @@ public class AmazonS3Service {
@Value("${cloud.aws.s3.bucket}")
private String bucket;

public String uploadFile(MultipartFile profileImg) {
String fileName = generateFileName(profileImg.getOriginalFilename());
@Getter
@Value("${cloud.aws.s3.default.profile.image}")
private String defaultProfileImg;

public String uploadFile(MultipartFile file, DomainType type) {

String ext = ImageUtil.getExt(file.getOriginalFilename()); // 확장자
String fileName = UUID.randomUUID() + "." + ext; // 파일 이름 + 확장자
String imageFilePath = generateFilePath(fileName, type); // 타입/날짜/파일이름

try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(profileImg.getSize());
metadata.setContentType(profileImg.getContentType());
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());

uploadToS3(bucket, fileName, profileImg, metadata);
return fileName;
uploadToS3(bucket, imageFilePath, file, metadata);
return imageFilePath;

} catch (IOException e) {
throw new AwsS3Exception(ErrorCode.AWS_S3_FILE_UPLOAD_FAIL);
}
}

// TODO: 회원, 책 이미지 업로드 시 공통으로 사용할 수 있게 리팩토링 필요함.
public String uploadBookCover(MultipartFile profileImg) {
String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String fileName = String.format("books/%s/%s", currentDate, profileImg.getOriginalFilename());

try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(profileImg.getSize());
metadata.setContentType(profileImg.getContentType());

uploadToS3(bucket, fileName, profileImg, metadata);
return fileName;

} catch (IOException e) {
throw new AwsS3Exception(ErrorCode.AWS_S3_FILE_UPLOAD_FAIL);
}
private void uploadToS3(String bucket, String filePath, MultipartFile file, ObjectMetadata metadata)
throws IOException {
s3Client.putObject(new PutObjectRequest(bucket, filePath, file.getInputStream(), metadata));
}

public void deleteFile(String fileName) {
public void deleteFile(String filePath) {
try {
s3Client.deleteObject(new DeleteObjectRequest(bucket, fileName));
log.debug("삭제한 이미지 파일 이름: {}", fileName);
s3Client.deleteObject(new DeleteObjectRequest(bucket, filePath));
log.debug("삭제한 이미지 파일의 경로: {}", filePath);
} catch (AmazonServiceException e) {
throw new AwsS3Exception(ErrorCode.AWS_S3_FILE_DELETE_FAIL);
}
}

public String getFileUrl(String fileName) {
return s3Client.getUrl(bucket, fileName).toString();
}

private String generateFileName(String originalFileName) {
String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
return String.format("members/%s/%s", currentDate, originalFileName);
// [type]/[yyyy_MM_dd]/[파일 이름] 형식의 경로 반환 메서드
private String generateFilePath(String fileName, DomainType type) {
String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy_MM_dd"));
return String.format("%s/%s/%s", type.getType(), currentDate, fileName);
}

private void uploadToS3(String bucket, String fileName, MultipartFile file, ObjectMetadata metadata)
throws IOException {
s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), metadata));
// 전체 이미지 주소 반환 메서드
public String getFileUrl(String fileName) {
return s3Client.getUrl(bucket, fileName).toString();
}

}
13 changes: 13 additions & 0 deletions src/main/java/com/api/readinglog/common/aws/DomainType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.api.readinglog.common.aws;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum DomainType {

MEMBERS("members"), BOOK("books");

private final String type;
}
10 changes: 8 additions & 2 deletions src/main/java/com/api/readinglog/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public enum ErrorCode {

// 400
PASSWORD_MISMATCH("비밀번호가 일치하지 않습니다.", HttpStatus.BAD_REQUEST),
INVALID_CURRENT_PASSWORD("현재 비밀번호와 일치하지 않습니다.", HttpStatus.BAD_REQUEST),
INVALID_AUTH_CODE("이메일 인증에 실패했습니다.", HttpStatus.BAD_REQUEST),
NOT_FOUND_REFRESH_TOKEN_IN_COOKIE("리프레시 토큰이 쿠키에 없습니다.", HttpStatus.BAD_REQUEST),

// 401
UNAUTHORIZED_LOGIN("로그인 실패: 인증에 실패하였습니다.", HttpStatus.UNAUTHORIZED),
Expand All @@ -32,10 +35,12 @@ public enum ErrorCode {
NOT_FOUND_MEMBER("회원이 존재하지 않습니다!", HttpStatus.NOT_FOUND),
NOT_FOUND_SEARCH("검색 결과가 존재하지 않습니다!", HttpStatus.NOT_FOUND),
NOT_FOUND_BOOK("등록된 책이 존재하지 않습니다!", HttpStatus.NOT_FOUND),
NOT_FOUND_HIGHLIGHT("등록된 책이 존재하지 않습니다!", HttpStatus.NOT_FOUND),
NOT_FOUND_HIGHLIGHT("등록된 하이라이트가 존재하지 않습니다!", HttpStatus.NOT_FOUND),
NOT_FOUND_SUMMARY("등록된 한줄평이 존재하지 않습니다!", HttpStatus.NOT_FOUND),
NOT_FOUND_REVIEW("서평이 존재하지 않습니다!", HttpStatus.NOT_FOUND),
NOT_FOUND_RECORD("독서 기록이 존재하지 않습니다!", HttpStatus.NOT_FOUND),
NOT_FOUND_FEED("피드 목록이 존재하지 않습니다!", HttpStatus.NOT_FOUND),
NOT_FOUND_BOOK_LOGS("북로그 목록이 존재하지 않습니다!", HttpStatus.NOT_FOUND),

// 409
MEMBER_ALREADY_EXISTS("이미 존재하는 회원입니다.", HttpStatus.CONFLICT),
Expand All @@ -45,7 +50,8 @@ public enum ErrorCode {
// 500
INTERNAL_SERVER_ERROR("서버 에러 발생!", HttpStatus.INTERNAL_SERVER_ERROR),
AWS_S3_FILE_UPLOAD_FAIL("AWS S3 파일 업로드 실패", HttpStatus.INTERNAL_SERVER_ERROR),
AWS_S3_FILE_DELETE_FAIL("AWS S3 파일 삭제 실패", HttpStatus.INTERNAL_SERVER_ERROR),;
AWS_S3_FILE_DELETE_FAIL("AWS S3 파일 삭제 실패", HttpStatus.INTERNAL_SERVER_ERROR),
EMAIL_SEND_FAILED("이메일 발송 실패", HttpStatus.INTERNAL_SERVER_ERROR);

private final String message;
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.api.readinglog.common.exception.custom;

import com.api.readinglog.common.exception.CustomException;
import com.api.readinglog.common.exception.ErrorCode;

public class EmailException extends CustomException {

public EmailException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.api.readinglog.common.exception.custom;

import com.api.readinglog.common.exception.CustomException;
import com.api.readinglog.common.exception.ErrorCode;

public class RecordException extends CustomException {
public RecordException(ErrorCode errorCode) {
super(errorCode);
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/api/readinglog/common/image/ImageUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.api.readinglog.common.image;

import java.util.Optional;
import org.springframework.web.multipart.MultipartFile;

public class ImageUtil {

// 파일의 확장자를 반환해주는 메서드
public static String getExt(String fileName) {
return Optional.ofNullable(fileName)
.filter(f -> f.contains("."))
.map(f -> f.substring(fileName.lastIndexOf(".") + 1))
.orElse("");
}

// 이미지 존재 여부 확인
public static boolean isNotEmptyImageFile(MultipartFile file) {
return !(file == null || file.isEmpty());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.api.readinglog.common.redis.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableRedisRepositories
public class RedisConfig {

@Value("${spring.redis.host}")
private String redisHost;

@Value("${spring.redis.port}")
private int redisPort;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.api.readinglog.common.redis.service;

import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor

public class RedisService {

private final RedisTemplate<String, Object> redisTemplate;


public void setData(String key, Object value, Long time, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value.toString(), time, timeUnit);
}

public Object getData(String key) {
return redisTemplate.opsForValue().get(key);
}


public void deleteData(String key) {
redisTemplate.delete(key);
}


public void increaseData(String key) {
redisTemplate.opsForValue().increment(key);
}

}
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
package com.api.readinglog.common.security.util;

import com.api.readinglog.common.exception.ErrorCode;
import com.api.readinglog.common.exception.custom.JwtException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Base64;
import java.util.Optional;
import org.springframework.util.SerializationUtils;

public class CookieUtils {

public static Optional<Cookie> getCookie(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();

if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return Optional.of(cookie);
}
}
}
return Optional.empty();
}

public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
Expand All @@ -31,19 +19,16 @@ public static void addCookie(HttpServletResponse response, String name, String v
response.addCookie(cookie);
}

public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
public static String extractRefreshToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();

if (cookies != null && cookies.length > 0) {
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
cookie.setValue("");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
if (cookie.getName().equals("refreshToken")) {
return cookie.getValue();
}
}
}
throw new JwtException(ErrorCode.NOT_FOUND_REFRESH_TOKEN_IN_COOKIE);
}

public static String serialize(Object obj) {
Expand Down
45 changes: 45 additions & 0 deletions src/main/java/com/api/readinglog/common/swagger/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.api.readinglog.common.swagger;


import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import java.util.Arrays;
import java.util.Collections;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@OpenAPIDefinition(
servers = {@Server(url = "/")},
info = @Info(
title = "리딩로그 API 명세서",
description = "독서 기록 서비스 리딩로그의 API 명세서",
version = "v1.0")
)
@Configuration
public class SwaggerConfig {

@Bean
public OpenAPI openAPI(){
String securityRequirementName = "Bearer를 제외한 accessToken값을 넣어주세요.";
SecurityRequirement securityRequirement = new SecurityRequirement().addList(securityRequirementName);

Components components = new Components()
.addSecuritySchemes(securityRequirementName, new SecurityScheme()
.name(securityRequirementName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT"));

return new OpenAPI()
.components(components)
.addSecurityItem(securityRequirement);
}
}
Loading

0 comments on commit 7f9e870

Please sign in to comment.