Skip to content

Commit

Permalink
Merge pull request #43 from KTB16Team/feature/41-S3-presignedUrl
Browse files Browse the repository at this point in the history
[feature] s3 presigned url 및 speech-to-text API 구현
  • Loading branch information
mng990 authored Nov 3, 2024
2 parents 9ade0cc + 29010f1 commit de9aee9
Show file tree
Hide file tree
Showing 50 changed files with 665 additions and 166 deletions.
2 changes: 2 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
//DB
runtimeOnly 'com.h2database:h2'
//AWS
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
//swagger
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.5.0'
testImplementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-api', version: '2.5.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
import aimo.backend.common.properties.AiServerProperties;
import aimo.backend.common.properties.CorsProperties;
import aimo.backend.common.properties.JwtProperties;
import aimo.backend.common.properties.S3Properties;
import aimo.backend.common.properties.SecurityProperties;

@Configuration
@EnableConfigurationProperties(value = {
JwtProperties.class,
SecurityProperties.class,
CorsProperties.class,
AiServerProperties.class
AiServerProperties.class,
S3Properties.class
})
public class PropertiesConfig {
}
35 changes: 35 additions & 0 deletions backend/src/main/java/aimo/backend/common/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package aimo.backend.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

import aimo.backend.common.properties.S3Properties;
import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class S3Config {

private final S3Properties s3Properties;

@Bean
public AmazonS3 amazonS3Client(){
BasicAWSCredentials awsCreds = new BasicAWSCredentials(
s3Properties.getAccessKey(),
s3Properties.getSecretKey()
);

return AmazonS3ClientBuilder
.standard()
.withRegion(s3Properties.getRegion())
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
Expand Down Expand Up @@ -67,6 +68,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.addFilterBefore(jwtAuthenticationFilter(), LoginFilter.class)
.addFilterBefore(exceptionHandlingFilter(), JwtAuthenticationFilter.class);

// X-frame option 해제 (h2 인메모리 DB 사용시 활성화)
http
.headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
);

return http.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public static <T> DataResponse<Void> created() {
return new DataResponse<>(HttpStatus.CREATED, null);
}

public static <T> DataResponse<T> created(T data) {
return new DataResponse<>(HttpStatus.CREATED, data);
}

public static <T> DataResponse<Void> noContent() {
return new DataResponse<>(HttpStatus.NO_CONTENT, null);
}
Expand Down
22 changes: 13 additions & 9 deletions backend/src/main/java/aimo/backend/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ public enum ErrorCode {
EMAIL_NOT_MATCH(HttpStatus.BAD_REQUEST, "이메일이 일치하지 않습니다.", "MEMBER-010"),

//PrivatePost
PRIVATE_POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 대화록을 찾을 수 없습니다.", "DISPUTE-001"),
PRIVATE_POST_CREATE_FAIL(HttpStatus.BAD_REQUEST, "대화록 생성에 실패하였습니다.", "DISPUTE-002"),
PRIVATE_POST_DELETE_FAIL(HttpStatus.BAD_REQUEST, "대화록 삭제에 실패하였습니다.", "DISPUTE-003"),
PRIVATE_POST_UPDATE_FAIL(HttpStatus.BAD_REQUEST, "대화록 수정에 실패하였습니다.", "DISPUTE-004"),
PRIVATE_POST_DELETE_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "대화록 삭제 권한이 없습니다.", "DISPUTE-006"),
PRIVATE_POST_CREATE_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "대화록 생성 권한이 없습니다.", "DISPUTE-007"),
PRIVATE_POST_UPDATE_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "대화록 수정 권한이 없습니다.", "DISPUTE-007"),
PRIVATE_POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 대화록을 찾을 수 없습니다.", "PRIVATEPOST-001"),
PRIVATE_POST_CREATE_FAIL(HttpStatus.BAD_REQUEST, "대화록 생성에 실패하였습니다.", "PRIVATEPOST-002"),
PRIVATE_POST_DELETE_FAIL(HttpStatus.BAD_REQUEST, "대화록 삭제에 실패하였습니다.", "PRIVATEPOST-003"),
PRIVATE_POST_UPDATE_FAIL(HttpStatus.BAD_REQUEST, "대화록 수정에 실패하였습니다.", "PRIVATEPOST-004"),
PRIVATE_POST_DELETE_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "대화록 삭제 권한이 없습니다.", "PRIVATEPOST-006"),
PRIVATE_POST_CREATE_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "대화록 생성 권한이 없습니다.", "PRIVATEPOST-007"),
PRIVATE_POST_UPDATE_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "대화록 수정 권한이 없습니다.", "PRIVATEPOST-007"),
PRIVATE_POST_READ_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "대화록 조회 권한이 없습니다.", "PRIVATEPOST-008"),

//Post
POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 게시글을 찾을 수 없습니다.", "POST-001"),
Expand Down Expand Up @@ -70,8 +71,11 @@ public enum ErrorCode {
AI_SEVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "AI 서버 내부에서 에러가 발생하였습니다.", "AI-002"),

// FILE
INVALID_FILE_NAME(HttpStatus.BAD_REQUEST, "파일 이름이 잘못 되었습니다.", "FILE-001"),
INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST, "파일 확장자가 잘못 되었습니다.", "FILE-002"),
INVALID_PREFIX(HttpStatus.BAD_REQUEST, "잘못된 파일 경로입니다.", "FILE-000"),
PREFIX_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 경로를 찾을 수 없습니다.", "FILE-001"),
PREFIX_IS_NULL(HttpStatus.BAD_REQUEST, "파일 경로가 비어있습니다.", "FILE-002"),
INVALID_FILE_NAME(HttpStatus.BAD_REQUEST, "파일 이름이 잘못 되었습니다.", "FILE-003"),
INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST, "파일 확장자가 잘못 되었습니다.", "FILE-004"),

;
private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package aimo.backend.common.mapper;

import aimo.backend.domains.privatePost.dto.SaveAudioSuccessRequest;
import aimo.backend.domains.privatePost.dto.SaveAudioSuccessResponse;
import aimo.backend.domains.privatePost.entity.AudioRecord;

public class AudioRecordMapper {
Expand All @@ -13,4 +14,8 @@ public static AudioRecord toEntity(SaveAudioSuccessRequest saveAudioSuccessReque
.filename(saveAudioSuccessRequest.filename())
.build();
}

public static SaveAudioSuccessResponse toSaveAudioSuccessResponse(AudioRecord audioRecord) {
return new SaveAudioSuccessResponse(audioRecord.getUrl(), audioRecord.getSize(), audioRecord.getFilename());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class MemberMapper {
public Member signUpMemberEntity(SignUpRequest signUpRequest) {
return Member
.builder()
.username(signUpRequest.username())
.memberName(signUpRequest.memberName())
.password(passwordEncoder.encode(signUpRequest.password()))
.email(signUpRequest.email())
.memberRole(MemberRole.USER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public class AiServerProperties {

private String domainUrl;
private String judgementApi;
private String speechToTextApi;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package aimo.backend.common.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "cloud.aws.s3")
public class S3Properties {
private String accessKey;
private String secretKey;
private String region;
private String bucketName;

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import aimo.backend.domains.comment.service.ChildCommentService;
import aimo.backend.domains.member.entity.Member;
import aimo.backend.util.memberLoader.MemberLoader;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
Expand All @@ -26,9 +27,9 @@ public class ChildCommentController {

@PostMapping("/{postId}/comments/{parentCommentId}/child")
public ResponseEntity<DataResponse<Void>> saveChildComment(
@PathVariable("postId") Long postId,
@PathVariable("parentCommentId") Long parentCommentId,
@RequestBody SaveChildCommentRequest request
@Valid @PathVariable("postId") Long postId,
@Valid @PathVariable("parentCommentId") Long parentCommentId,
@Valid @RequestBody SaveChildCommentRequest request
) {
Member member = memberLoader.getMember();
childCommentService.saveChildComment(member, postId, parentCommentId, request);
Expand All @@ -38,8 +39,8 @@ public ResponseEntity<DataResponse<Void>> saveChildComment(

@PutMapping("comments/child/{childCommentId}")
public ResponseEntity<DataResponse<Void>> updateParentComment(
@PathVariable Long childCommentId,
@RequestBody SaveChildCommentRequest request
@Valid @PathVariable Long childCommentId,
@Valid @RequestBody SaveChildCommentRequest request
) {
Member member = memberLoader.getMember();
childCommentService.validateAndUpdateChildComment(member, childCommentId, request);
Expand All @@ -49,7 +50,7 @@ public ResponseEntity<DataResponse<Void>> updateParentComment(

@DeleteMapping("comments/child/{childCommentId}")
public ResponseEntity<DataResponse<Void>> deleteParentComment(
@PathVariable Long childCommentId
@Valid @PathVariable Long childCommentId
) {
Member member = memberLoader.getMember();
childCommentService.validateAndDeleteChildComment(member, childCommentId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import aimo.backend.domains.comment.service.ParentCommentService;
import aimo.backend.domains.member.entity.Member;
import aimo.backend.util.memberLoader.MemberLoader;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
Expand All @@ -27,8 +28,8 @@ public class ParentCommentController {

@PostMapping("/{postId}/comments")
public ResponseEntity<DataResponse<Void>> saveParentComment(
@PathVariable Long postId,
@RequestBody SaveParentCommentRequest request
@Valid @PathVariable Long postId,
@Valid @RequestBody SaveParentCommentRequest request
) {
Member member = memberLoader.getMember();
parentCommentService.saveParentComment(member, postId, request);
Expand All @@ -38,8 +39,8 @@ public ResponseEntity<DataResponse<Void>> saveParentComment(

@PutMapping("comments/{commentId}")
public ResponseEntity<DataResponse<Void>> updateParentComment(
@PathVariable Long commentId,
@RequestBody UpdateParentCommentRequest request
@Valid @PathVariable Long commentId,
@Valid @RequestBody UpdateParentCommentRequest request
) {
Member member = memberLoader.getMember();
parentCommentService.validateAndUpdateParentComment(member, commentId, request);
Expand All @@ -49,7 +50,7 @@ public ResponseEntity<DataResponse<Void>> updateParentComment(

@DeleteMapping("comments/{commentId}")
public ResponseEntity<DataResponse<Void>> deleteParentComment(
@PathVariable Long commentId
@Valid @PathVariable Long commentId
) {
Member member = memberLoader.getMember();
parentCommentService.validateAndDeleteParentComment(member, commentId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class ChildCommentMapper {
public ChildComment from(SaveChildCommentRequest request,
Member member, ParentComment parentComment, Post post) {
return ChildComment.builder()
.memberName(member.getUsername())
.memberName(member.getMemberName())
.content(request.content())
.parentComment(parentComment)
.isDeleted(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class ParentCommentMapper {

public ParentComment from(SaveParentCommentRequest request, Member member, Post post) {
return ParentComment.builder()
.memberName(member.getUsername())
.memberName(member.getMemberName())
.content(request.content())
.isDeleted(false)
.member(member)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RestController;

import aimo.backend.common.dto.DataResponse;
import aimo.backend.domains.auth.security.jwtFilter.JwtTokenProvider;
import aimo.backend.domains.member.dto.CreateProfileImageUrlRequest;
import aimo.backend.domains.member.dto.DeleteRequest;
import aimo.backend.domains.member.dto.LogOutRequest;
import aimo.backend.domains.member.dto.SignUpRequest;
import aimo.backend.common.dto.DataResponse;
import aimo.backend.domains.auth.security.jwtFilter.JwtTokenProvider;
import aimo.backend.infrastructure.s3.S3Service;
import aimo.backend.infrastructure.s3.dto.CreatePresignedUrlResponse;
import aimo.backend.infrastructure.s3.dto.SaveFileMetaDataRequest;

import aimo.backend.domains.member.service.MemberService;
import aimo.backend.util.memberLoader.MemberLoader;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -29,14 +37,14 @@ public class MemberController {

private final MemberService memberService;
private final JwtTokenProvider jwtTokenProvider;
private final MemberLoader memberLoader;
private final S3Service s3Service;

@PostMapping("/logout")
public ResponseEntity<DataResponse<Void>> logoutMember(HttpServletRequest request) {
String accessToken = jwtTokenProvider.extractAccessToken(request).orElse(null);
String refreshToken = jwtTokenProvider.extractRefreshToken(request).orElse(null);
log.info("logout member access token: {} refresh token: {}", accessToken, refreshToken);
memberService.logoutMember(accessToken, refreshToken);
memberService.logoutMember(new LogOutRequest(accessToken, refreshToken));

return ResponseEntity
.status(HttpStatus.CREATED)
Expand All @@ -56,7 +64,34 @@ public ResponseEntity<DataResponse<Void>> signupMember(@RequestBody @Valid SignU
public ResponseEntity<DataResponse<Void>> deleteMember(
@RequestHeader("Authorization") String accessToken,
@RequestBody DeleteRequest deleteRequest) {
memberService.deleteMember(accessToken, deleteRequest);
memberService.deleteMember(deleteRequest);

return ResponseEntity
.status(HttpStatus.NO_CONTENT)
.body(DataResponse.noContent());
}

@PostMapping("/profile/presigned")
public ResponseEntity<DataResponse<CreatePresignedUrlResponse>> createProfileImagePreSignedUrl(
@RequestBody CreateProfileImageUrlRequest request) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(DataResponse.created(s3Service.createProfilePresignedUrl(request)));
}

@PostMapping("/profile/success")
public ResponseEntity<DataResponse<Void>> saveProfileImageMetaData(
@RequestBody SaveFileMetaDataRequest request) {
memberService.saveProfileImageMetaData(request);

return ResponseEntity
.status(HttpStatus.CREATED)
.body(DataResponse.created());
}

@DeleteMapping("/profile")
public ResponseEntity<DataResponse<Void>> deleteProfileImage() {
memberService.deleteProfileImage();

return ResponseEntity
.status(HttpStatus.NO_CONTENT)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package aimo.backend.domains.member.dto;

public record CreateProfileImageUrlRequest(
String memberName,
String extension
) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package aimo.backend.domains.member.dto;

import aimo.backend.domains.member.entity.Member;

public record DeleteRequest(String password) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package aimo.backend.domains.member.dto;

public record LogOutRequest(
String accessToken,
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package aimo.backend.domains.member.dto;

public record SaveProfileImageMetaData(
String filename,
String extension,
Long size,
String url
) {
}
Loading

0 comments on commit de9aee9

Please sign in to comment.