diff --git a/build.gradle b/build.gradle index 929c373..3a61b70 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'mysql:mysql-connector-java:8.0.25' implementation 'org.springdoc:springdoc-openapi-ui:1.7.0' + implementation 'org.springframework.boot:spring-boot-starter-webflux' // WebClient + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' // S3 compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/HeyPorori/transaction/TransactionApplication.java b/src/main/java/HeyPorori/transaction/TransactionApplication.java index 72b163c..c5c0269 100644 --- a/src/main/java/HeyPorori/transaction/TransactionApplication.java +++ b/src/main/java/HeyPorori/transaction/TransactionApplication.java @@ -2,10 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class TransactionApplication { - + static { + System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true"); // S3 예외발생 방지 + } public static void main(String[] args) { SpringApplication.run(TransactionApplication.class, args); } diff --git a/src/main/java/HeyPorori/transaction/config/AmazonS3Config.java b/src/main/java/HeyPorori/transaction/config/AmazonS3Config.java new file mode 100644 index 0000000..8a767f4 --- /dev/null +++ b/src/main/java/HeyPorori/transaction/config/AmazonS3Config.java @@ -0,0 +1,36 @@ +package HeyPorori.transaction.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 org.springframework.context.annotation.Primary; + +@Configuration +public class AmazonS3Config { + @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 + @Primary + public BasicAWSCredentials awsCredentialsProvider(){ + return new BasicAWSCredentials(accessKey, secretKey); + } + + @Bean + public AmazonS3 amazonS3Client() { + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider())) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/HeyPorori/transaction/config/BaseException.java b/src/main/java/HeyPorori/transaction/config/BaseException.java index 104a589..44a7662 100644 --- a/src/main/java/HeyPorori/transaction/config/BaseException.java +++ b/src/main/java/HeyPorori/transaction/config/BaseException.java @@ -2,11 +2,13 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @AllArgsConstructor -public class BaseException extends Exception { +@NoArgsConstructor +public class BaseException extends RuntimeException { private BaseResponseStatus status; } \ No newline at end of file diff --git a/src/main/java/HeyPorori/transaction/config/BaseResponse.java b/src/main/java/HeyPorori/transaction/config/BaseResponse.java index 109f396..91a9262 100644 --- a/src/main/java/HeyPorori/transaction/config/BaseResponse.java +++ b/src/main/java/HeyPorori/transaction/config/BaseResponse.java @@ -4,11 +4,13 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import static HeyPorori.transaction.config.BaseResponseStatus.SUCCESS; @Getter @AllArgsConstructor +@NoArgsConstructor @JsonPropertyOrder({"statusCode", "message", "data"}) public class BaseResponse { private String message; diff --git a/src/main/java/HeyPorori/transaction/config/BaseResponseStatus.java b/src/main/java/HeyPorori/transaction/config/BaseResponseStatus.java index c40cc08..0651544 100644 --- a/src/main/java/HeyPorori/transaction/config/BaseResponseStatus.java +++ b/src/main/java/HeyPorori/transaction/config/BaseResponseStatus.java @@ -5,7 +5,15 @@ @Getter public enum BaseResponseStatus { /* 요청 성공 */ - SUCCESS(true, 1000, "요청에 성공하였습니다.") + SUCCESS(true, 1000, "요청에 성공하였습니다."), + + /* 요청 실패 */ + USER_INVALID_RESPONSE(false, 1050, "User 서비스로부터 올바른 응답을 받지 못하였습니다."), + INVALID_JWT(false, 1051, "잘못된 JWT 토큰입니다."), + INVALID_CATEGORY(false, 3001, "유효하지 않은 카테고리입니다."), + POST_NOT_FOUND(false, 3010, "존재하지 않는 게시글입니다."), + INVALID_POST_OWNER(false, 3011, "게시글 작성자가 아닙니다.") + ; private final boolean isSuccess; diff --git a/src/main/java/HeyPorori/transaction/config/SwaggerConfig.java b/src/main/java/HeyPorori/transaction/config/SwaggerConfig.java index 1b6dc61..d23e2e5 100644 --- a/src/main/java/HeyPorori/transaction/config/SwaggerConfig.java +++ b/src/main/java/HeyPorori/transaction/config/SwaggerConfig.java @@ -1,11 +1,21 @@ package HeyPorori.transaction.config; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration +@SecurityScheme( + type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, + name = "Authorization", description = "Authorization" +) +@OpenAPIDefinition(security = { @SecurityRequirement(name = "Authorization") }) public class SwaggerConfig { @Bean public OpenAPI customOpenAPI() { diff --git a/src/main/java/HeyPorori/transaction/config/WebClientConfig.java b/src/main/java/HeyPorori/transaction/config/WebClientConfig.java new file mode 100644 index 0000000..cfe40c7 --- /dev/null +++ b/src/main/java/HeyPorori/transaction/config/WebClientConfig.java @@ -0,0 +1,26 @@ +package HeyPorori.transaction.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + @Value("${service.user}") + private String userUrl; + @Bean + public ReactorResourceFactory reactorResourceFactory(){ + ReactorResourceFactory factory = new ReactorResourceFactory(); + factory.setUseGlobalResources(false); + return factory; + } + + @Bean + public WebClient webClient(){ + return WebClient.builder() + .baseUrl(userUrl) + .build(); + } +} diff --git a/src/main/java/HeyPorori/transaction/controller/TransactionController.java b/src/main/java/HeyPorori/transaction/controller/TransactionController.java index 3fb02af..1c74ee5 100644 --- a/src/main/java/HeyPorori/transaction/controller/TransactionController.java +++ b/src/main/java/HeyPorori/transaction/controller/TransactionController.java @@ -2,17 +2,21 @@ import HeyPorori.transaction.config.BaseException; import HeyPorori.transaction.config.BaseResponse; +import HeyPorori.transaction.config.BaseResponseStatus; +import HeyPorori.transaction.dto.*; +import HeyPorori.transaction.service.AmazonS3Service; import HeyPorori.transaction.service.TransactionService; +import HeyPorori.transaction.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; @RequiredArgsConstructor @Tag(name = "중고거래", description = "중고거래 관련 API 입니다.") @@ -20,11 +24,73 @@ @RequestMapping("/api/transactions") public class TransactionController { private final TransactionService transactionService; + private final UserService userService; + private final AmazonS3Service amazonS3Service; + // 테스트용 APIs @Operation(summary = "Swagger UI 테스트용 메서드", description = "프로젝트 초기 Swagger UI 정상작동을 확인하기 위한 메서드입니다.") @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class))) @GetMapping("/test") - public BaseResponse testSwagger() { - return new BaseResponse<>("테스트 성공"); + public BaseResponse testSwagger() { return new BaseResponse<>("테스트 성공"); } + + @Operation(summary = "User 서버 테스트용 메서드", description = "User 서버와의 통신이 정상작동하는지 확인하기 위한 메서드입니다.") + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class))) + @PostMapping("/testUser") + public BaseResponse testUserService(@RequestHeader("Authorization") String token) { + userService.sendTestJwtRequest(token); + return new BaseResponse<>(userService.getUserId(token)); + } + + // 실사용 APIs + @Operation(summary = "중고거래 게시글 작성 API", description = "중고거래 서비스의 거래 게시글을 작성하기 위한 API입니다.") + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class))) + @PostMapping("/post") + public BaseResponse createPost(@RequestHeader("Authorization") String token, @RequestBody @Valid CreatePostReq postReq) { + transactionService.createPost(token, postReq); + return new BaseResponse<>(BaseResponseStatus.SUCCESS); + } + + @Operation(summary = "Pre-Signed Url 발급 API", description = "AWS S3 이미지 업로드 권한을 요청하기 위한 API입니다.") + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class))) + @GetMapping("/url") + public BaseResponse getPreSignedUrl(@RequestHeader("Authorization") String token) throws BaseException { + return new BaseResponse<>(amazonS3Service.getPreSignedUrl()); + } + + @Operation(summary = "중고거래 게시글 목록 조회 API", description = "중고거래 서비스의 거래 게시글 목록을 조회하기 위한 API입니다.") + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class))) + @GetMapping("/post") + public BaseResponse> getAllPostByCategory(String category) throws BaseException { + return new BaseResponse<>(transactionService.findAllPostByCategory(category)); + } + + @Operation(summary = "중고거래 게시글 상세 조회 API", description = "중고거래 서비스의 거래 게시글 상세정보를 조회하기 위한 API입니다.") + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class))) + @GetMapping("/post/{transactionId}") + public BaseResponse getPostDetail(@RequestHeader("Authorization") String token, @PathVariable Long transactionId) throws BaseException { + return new BaseResponse<>(transactionService.getPostDetail(token, transactionId)); + } + + @Operation(summary = "중고거래 게시글 삭제 API", description = "중고거래 서비스의 거래 게시글을 삭제하기 위한 API입니다.") + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class))) + @DeleteMapping("/post/{transactionId}") + public BaseResponse deletePost(@RequestHeader("Authorization") String token, @PathVariable Long transactionId) throws BaseException { + transactionService.deletePost(token, transactionId); + return new BaseResponse<>("게시글이 삭제되었습니다."); + } + + @Operation(summary = "중고거래 게시글 추천 API", description = "중고거래 서비스의 거래 게시글을 추천하거나 추천 취소하기 위한 API입니다.") + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class))) + @PostMapping("/recommend") + public BaseResponse recommendPost(@RequestHeader("Authorization") String token, @RequestBody RecommendPostReq req) throws BaseException { + transactionService.recommendPost(token, req); + return new BaseResponse<>("게시글을 추천했습니다."); + } + + @Operation(summary = "중고거래 게시글 검색 API", description = "중고거래 서비스의 거래 게시글을 검색하기 위한 API입니다.") + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BaseResponse.class))) + @GetMapping("/search/{keyword}") + public BaseResponse> recommendPost(@PathVariable String keyword) throws BaseException { + return new BaseResponse<>(transactionService.findAllPostByKeyword(keyword)); } -} +} \ No newline at end of file diff --git a/src/main/java/HeyPorori/transaction/domain/Category.java b/src/main/java/HeyPorori/transaction/domain/Category.java index 7bb5bcc..6b9b327 100644 --- a/src/main/java/HeyPorori/transaction/domain/Category.java +++ b/src/main/java/HeyPorori/transaction/domain/Category.java @@ -1,7 +1,13 @@ package HeyPorori.transaction.domain; +import HeyPorori.transaction.config.BaseException; +import HeyPorori.transaction.config.BaseResponseStatus; +import com.fasterxml.jackson.annotation.JsonCreator; import lombok.Getter; +import java.util.Arrays; +import java.util.stream.Stream; + @Getter public enum Category { ELECTRONICS("전자제품", "스마트폰, 컴퓨터, 태블릿, TV, 오디오 기기 등"), @@ -20,4 +26,10 @@ public enum Category { this.name = name; this.description = description; } + + public static Category parsing(String inputValue) { + return Arrays.stream(Category.values()) + .filter(r -> r.name.equals(inputValue)) + .findAny().orElse(null); + } } \ No newline at end of file diff --git a/src/main/java/HeyPorori/transaction/domain/Recommend.java b/src/main/java/HeyPorori/transaction/domain/Recommend.java index 273ebd1..eb9f302 100644 --- a/src/main/java/HeyPorori/transaction/domain/Recommend.java +++ b/src/main/java/HeyPorori/transaction/domain/Recommend.java @@ -1,7 +1,11 @@ package HeyPorori.transaction.domain; import HeyPorori.transaction.config.BaseTimeEntity; +import HeyPorori.transaction.dto.CreatePostReq; import lombok.*; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; import javax.persistence.*; @@ -10,6 +14,8 @@ @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "recommend") +@DynamicInsert +@DynamicUpdate public class Recommend extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -23,9 +29,24 @@ public class Recommend extends BaseTimeEntity { @Column(name = "user_id", nullable = false) private Long userId; + @ColumnDefault("'ACTIVE'") + private String status; + @Builder - public Recommend(Transaction transactionId, Long userId){ + public Recommend(Transaction transactionId, Long userId, String status){ this.transactionId = transactionId; this.userId = userId; + this.status = status; + } + + public static Recommend toEntity(Transaction transaction, Long userId){ + return Recommend.builder() + .transactionId(transaction) + .userId(userId) + .build(); + } + + public void changeStatus(String status) { + this.status = status; } } \ No newline at end of file diff --git a/src/main/java/HeyPorori/transaction/domain/Transaction.java b/src/main/java/HeyPorori/transaction/domain/Transaction.java index df465e7..c20e918 100644 --- a/src/main/java/HeyPorori/transaction/domain/Transaction.java +++ b/src/main/java/HeyPorori/transaction/domain/Transaction.java @@ -1,6 +1,7 @@ package HeyPorori.transaction.domain; import HeyPorori.transaction.config.BaseTimeEntity; +import HeyPorori.transaction.dto.CreatePostReq; import lombok.*; import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.DynamicInsert; @@ -44,7 +45,8 @@ public class Transaction extends BaseTimeEntity { @Column(nullable = false) private Double longitude; - @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + @Column(nullable = true) private Category category; @ColumnDefault("'ACTIVE'") @@ -69,6 +71,19 @@ public Transaction(Long userId, String title, String content, int recommend, Str this.status = status; } + public static Transaction toEntity(CreatePostReq postReq, Long userId){ + return Transaction.builder() + .userId(userId) + .title(postReq.getTitle()) + .content(postReq.getContent()) + .recommend(0) + .address("필동") + .latitude(0.0) + .longitude(0.0) + .category(Category.parsing(postReq.getCategory())) + .build(); + } + public void changeStatus(String status) { this.status = status; } diff --git a/src/main/java/HeyPorori/transaction/domain/TransactionAttach.java b/src/main/java/HeyPorori/transaction/domain/TransactionAttach.java index 75f7490..852efe2 100644 --- a/src/main/java/HeyPorori/transaction/domain/TransactionAttach.java +++ b/src/main/java/HeyPorori/transaction/domain/TransactionAttach.java @@ -30,4 +30,11 @@ public TransactionAttach(Transaction transactionId, String imageUrl){ this.transactionId = transactionId; this.imageUrl = imageUrl; } + + public static TransactionAttach toEntity(Transaction transaction, String imageUrl){ + return TransactionAttach.builder() + .transactionId(transaction) + .imageUrl(imageUrl) + .build(); + } } diff --git a/src/main/java/HeyPorori/transaction/domain/UserInfo.java b/src/main/java/HeyPorori/transaction/domain/UserInfo.java new file mode 100644 index 0000000..51a0fac --- /dev/null +++ b/src/main/java/HeyPorori/transaction/domain/UserInfo.java @@ -0,0 +1,22 @@ +package HeyPorori.transaction.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserInfo { + private Long userId; + private String name; + private String nickName; + private String phoneNumber; + private boolean gender; + private String address; + private String backgroundColor; + private String email; + private String imageUrl; +} diff --git a/src/main/java/HeyPorori/transaction/dto/CreatePostReq.java b/src/main/java/HeyPorori/transaction/dto/CreatePostReq.java new file mode 100644 index 0000000..ca5fd9b --- /dev/null +++ b/src/main/java/HeyPorori/transaction/dto/CreatePostReq.java @@ -0,0 +1,23 @@ +package HeyPorori.transaction.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class CreatePostReq { + @Size(min = 1, max = 20, message = "거래 게시글 제목의 길이는 1 이상 20 이하입니다.") + private String title; + @Size(min = 1, max = 200, message = "거래 게시글 내용의 길이는 1 이상 200 이하입니다.") + private String content; + @Pattern(regexp = "^(전자제품|의류 및 액세서리|가구 및 가정용품|스포츠 및 레저|자동차 및 오토바이|도서 및 음악|아기 및 어린이 용품|기타)$", message = "유효하지 않은 카테고리입니다.") + private String category; + @Size(max = 3, message = "거래 게시글에 첨부할 수 있는 사진은 3장 이하입니다.") + private List imageNameList; +} diff --git a/src/main/java/HeyPorori/transaction/dto/PostDetailRes.java b/src/main/java/HeyPorori/transaction/dto/PostDetailRes.java new file mode 100644 index 0000000..1753828 --- /dev/null +++ b/src/main/java/HeyPorori/transaction/dto/PostDetailRes.java @@ -0,0 +1,41 @@ +package HeyPorori.transaction.dto; + +import HeyPorori.transaction.domain.Transaction; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PostDetailRes { + private Long transactionId; + private String title; + private String content; + private String nickName; + private String address; + private String createdAt; + private int recommend; + private List imageNameList; + private boolean isOwner; + private boolean isRecommended; + + public static PostDetailRes toDto(Transaction transaction, String nickName, String createdAt, List imageNameList, boolean isOwner, boolean isRecommended){ + return PostDetailRes.builder() + .transactionId(transaction.getTransactionId()) + .title(transaction.getTitle()) + .content(transaction.getContent()) + .nickName(nickName) + .address(transaction.getAddress()) + .createdAt(createdAt) + .recommend(transaction.getRecommendList().size()) + .imageNameList(imageNameList) + .isOwner(isOwner) + .isRecommended(isRecommended) + .build(); + } +} diff --git a/src/main/java/HeyPorori/transaction/dto/PostsRes.java b/src/main/java/HeyPorori/transaction/dto/PostsRes.java new file mode 100644 index 0000000..bbc7df9 --- /dev/null +++ b/src/main/java/HeyPorori/transaction/dto/PostsRes.java @@ -0,0 +1,29 @@ +package HeyPorori.transaction.dto; + +import HeyPorori.transaction.domain.Transaction; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PostsRes { + private Long transactionId; + private String title; + private String imageName; + private String address; + private int recommend; + + public static PostsRes toDto(Transaction transaction, String imageName){ + return PostsRes.builder() + .transactionId(transaction.getTransactionId()) + .title(transaction.getTitle()) + .imageName(imageName) + .address(transaction.getAddress()) + .recommend(transaction.getRecommendList().size()) + .build(); + } +} diff --git a/src/main/java/HeyPorori/transaction/dto/PreSignedUrlRes.java b/src/main/java/HeyPorori/transaction/dto/PreSignedUrlRes.java new file mode 100644 index 0000000..7b2fad2 --- /dev/null +++ b/src/main/java/HeyPorori/transaction/dto/PreSignedUrlRes.java @@ -0,0 +1,14 @@ +package HeyPorori.transaction.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class PreSignedUrlRes { + private String url; + + @Builder + public PreSignedUrlRes(String url) { + this.url = url; + } +} diff --git a/src/main/java/HeyPorori/transaction/dto/RecommendPostReq.java b/src/main/java/HeyPorori/transaction/dto/RecommendPostReq.java new file mode 100644 index 0000000..5411dcb --- /dev/null +++ b/src/main/java/HeyPorori/transaction/dto/RecommendPostReq.java @@ -0,0 +1,19 @@ +package HeyPorori.transaction.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.util.List; +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class RecommendPostReq { + private Long transactionId; + @Pattern(regexp = "^(INACTIVE|ACTIVE)$", message = "유효하지 않은 상태입니다.") + private String toStatus; + @Pattern(regexp = "^(INACTIVE|ACTIVE)$", message = "유효하지 않은 상태입니다.") + private String fromStatus; +} \ No newline at end of file diff --git a/src/main/java/HeyPorori/transaction/dto/UserInfoRes.java b/src/main/java/HeyPorori/transaction/dto/UserInfoRes.java new file mode 100644 index 0000000..3464e49 --- /dev/null +++ b/src/main/java/HeyPorori/transaction/dto/UserInfoRes.java @@ -0,0 +1,20 @@ +package HeyPorori.transaction.dto; + +import HeyPorori.transaction.domain.Transaction; +import HeyPorori.transaction.domain.UserInfo; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class UserInfoRes { + private long userId; + private String nickName; + + public static UserInfoRes toDto(UserInfo userInfo){ + return UserInfoRes.builder() + .userId(userInfo.getUserId()) + .nickName(userInfo.getNickName()) + .build(); + } +} diff --git a/src/main/java/HeyPorori/transaction/repository/RecommendRepository.java b/src/main/java/HeyPorori/transaction/repository/RecommendRepository.java index c11f5f1..b92c657 100644 --- a/src/main/java/HeyPorori/transaction/repository/RecommendRepository.java +++ b/src/main/java/HeyPorori/transaction/repository/RecommendRepository.java @@ -1,9 +1,15 @@ package HeyPorori.transaction.repository; import HeyPorori.transaction.domain.Recommend; +import HeyPorori.transaction.domain.Transaction; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import javax.swing.text.html.Option; +import java.util.Optional; + @Repository public interface RecommendRepository extends JpaRepository { + boolean existsByTransactionIdAndUserId(Transaction transaction, Long userId); + Recommend findByTransactionIdAndUserIdAndStatus(Transaction transaction, Long userId, String status); } diff --git a/src/main/java/HeyPorori/transaction/repository/TransactionRepository.java b/src/main/java/HeyPorori/transaction/repository/TransactionRepository.java index 72eb3ae..33a2a68 100644 --- a/src/main/java/HeyPorori/transaction/repository/TransactionRepository.java +++ b/src/main/java/HeyPorori/transaction/repository/TransactionRepository.java @@ -1,9 +1,17 @@ package HeyPorori.transaction.repository; +import HeyPorori.transaction.domain.Category; import HeyPorori.transaction.domain.Transaction; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface TransactionRepository extends JpaRepository { + List findByStatus(String status); + Optional findByTransactionIdAndStatus(Long transactionId, String status); + List findByCategoryAndStatus(Category category, String status); + List findByContentContainingAndStatus(String content, String status); } diff --git a/src/main/java/HeyPorori/transaction/service/AmazonS3Service.java b/src/main/java/HeyPorori/transaction/service/AmazonS3Service.java new file mode 100644 index 0000000..0aef55b --- /dev/null +++ b/src/main/java/HeyPorori/transaction/service/AmazonS3Service.java @@ -0,0 +1,55 @@ +package HeyPorori.transaction.service; + +import HeyPorori.transaction.dto.PreSignedUrlRes; +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.Headers; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.Date; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Transactional +public class AmazonS3Service { + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public PreSignedUrlRes getPreSignedUrl(){ + String uuid = UUID.randomUUID().toString(); + String objectKey = "transactions/"+uuid; + + GeneratePresignedUrlRequest request = generatePresignedUrlRequest(bucket, objectKey); + PreSignedUrlRes response = PreSignedUrlRes.builder() + .url(amazonS3.generatePresignedUrl(request).toString()) + .build(); + return response; + } + + private GeneratePresignedUrlRequest generatePresignedUrlRequest(String bucket, String imageName){ + Date expiration = new Date(); + long expTimeMillis = expiration.getTime(); + expTimeMillis += 1000 * 60 * 5; // 5분 + expiration.setTime(expTimeMillis); + + //Pre-Signed Url request 생성 + GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, imageName) + .withMethod(HttpMethod.PUT) + .withExpiration(expiration); + + //request 파라미터 추가 + request.addRequestParameter( + Headers.S3_CANNED_ACL, + CannedAccessControlList.PublicRead.toString()); + + return request; + } +} \ No newline at end of file diff --git a/src/main/java/HeyPorori/transaction/service/TransactionAttachService.java b/src/main/java/HeyPorori/transaction/service/TransactionAttachService.java new file mode 100644 index 0000000..ec0ad43 --- /dev/null +++ b/src/main/java/HeyPorori/transaction/service/TransactionAttachService.java @@ -0,0 +1,44 @@ +package HeyPorori.transaction.service; + +import HeyPorori.transaction.domain.Transaction; +import HeyPorori.transaction.domain.TransactionAttach; +import HeyPorori.transaction.repository.TransactionAttachRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class TransactionAttachService { + private final TransactionAttachRepository transactionAttachRepository; + + public List savePostAttach(Transaction transaction, List imageNameList){ + List TransactionAttachList = new ArrayList<>(); + for(String imageName: imageNameList){ + TransactionAttachList.add(TransactionAttach.toEntity(transaction, imageName)); + } + return TransactionAttachList; + } + + public String getFirstImageName(Transaction transaction){ + List attachList = transaction.getAttachList(); + if(attachList.size() == 0){ + return ""; + } else{ + return attachList.get(0).getImageUrl(); + } + } + + public List getImageNameList(Transaction transaction){ + List attachList = transaction.getAttachList(); + List nameList = new ArrayList<>(); + for(TransactionAttach attach: attachList){ + nameList.add(attach.getImageUrl()); + } + return nameList; + } +} diff --git a/src/main/java/HeyPorori/transaction/service/TransactionService.java b/src/main/java/HeyPorori/transaction/service/TransactionService.java index c553bad..238d741 100644 --- a/src/main/java/HeyPorori/transaction/service/TransactionService.java +++ b/src/main/java/HeyPorori/transaction/service/TransactionService.java @@ -1,10 +1,109 @@ package HeyPorori.transaction.service; +import HeyPorori.transaction.config.BaseException; +import HeyPorori.transaction.config.BaseResponseStatus; +import HeyPorori.transaction.domain.Category; +import HeyPorori.transaction.domain.Recommend; +import HeyPorori.transaction.domain.Transaction; +import HeyPorori.transaction.dto.*; +import HeyPorori.transaction.repository.RecommendRepository; +import HeyPorori.transaction.repository.TransactionRepository; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import javax.transaction.Transactional; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; @Service @Transactional +@RequiredArgsConstructor public class TransactionService { + private final TransactionRepository transactionRepository; + private final UserService userService; + private final TransactionAttachService transactionAttachService; + private final RecommendRepository recommendRepository; + + public void createPost(String token, CreatePostReq postReq) { + userService.sendTestJwtRequest(token); + Transaction txn = Transaction.toEntity(postReq, userService.getUserId(token)); + txn.setAttachList(transactionAttachService.savePostAttach(txn, postReq.getImageNameList())); + transactionRepository.save(txn); + } + + public List findAllPostByCategory(String category){ + List txnList = new ArrayList<>(); + if(category.equals("NONE")){ + txnList = transactionRepository.findByStatus("ACTIVE"); + } else{ + txnList = transactionRepository.findByCategoryAndStatus(Category.parsing(category), "ACTIVE"); + } + + List postResList = new ArrayList<>(); + for(Transaction txn: txnList){ + PostsRes postRes = PostsRes.toDto(txn, transactionAttachService.getFirstImageName(txn)); + postResList.add(postRes); + } + + return postResList; + } + + public PostDetailRes getPostDetail(String token, Long transactionId){ + Transaction txn = transactionRepository.findByTransactionIdAndStatus(transactionId, "ACTIVE") + .orElseThrow(() -> new BaseException(BaseResponseStatus.POST_NOT_FOUND)); + UserInfoRes userInfoRes = userService.getUserIdAndNickName(token); + boolean isOwner = false; + if(userInfoRes.getUserId() == txn.getUserId()) isOwner = true; + List imageNameList = transactionAttachService.getImageNameList(txn); + boolean isRecommends = recommendRepository.existsByTransactionIdAndUserId(txn, userInfoRes.getUserId()); + PostDetailRes postDetailRes = PostDetailRes.toDto(txn, userInfoRes.getNickName(), toFormattedDate(txn.getCreatedAt()), imageNameList, isOwner, isRecommends); + return postDetailRes; + } + + public void deletePost(String token, Long transactionId){ + Long userId = userService.getUserId(token); + Transaction txn = transactionRepository.findByTransactionIdAndStatus(transactionId, "ACTIVE") + .orElseThrow(() -> new BaseException(BaseResponseStatus.POST_NOT_FOUND)); + if(userId != txn.getUserId()) throw new BaseException(BaseResponseStatus.INVALID_POST_OWNER); + txn.changeStatus("INACTIVE"); + transactionRepository.save(txn); + } + + public void recommendPost(String token, RecommendPostReq req){ + Long userId = userService.getUserId(token); + Transaction txn = transactionRepository.findByTransactionIdAndStatus(req.getTransactionId(), "ACTIVE") + .orElseThrow(() -> new BaseException(BaseResponseStatus.POST_NOT_FOUND)); + Recommend recommend = recommendRepository.findByTransactionIdAndUserIdAndStatus(txn, userId, req.getFromStatus()); + if(recommend == null && req.getToStatus().equals("ACTIVE")){ + Recommend newRecommend = Recommend.toEntity(txn, userId); + recommendRepository.save(newRecommend); + } else{ + if(recommend != null && recommend.getStatus().equals(req.getFromStatus())){ + recommend.changeStatus(req.getToStatus()); + } else{ + new IllegalArgumentException("추천 상태가 예상된 상태와 같지 않습니다."); + } + } + } + + public List findAllPostByKeyword(String keyword){ + List txnList = new ArrayList<>(); + txnList = transactionRepository.findByContentContainingAndStatus(keyword, "ACTIVE"); + + List postResList = new ArrayList<>(); + for(Transaction txn: txnList){ + PostsRes postRes = PostsRes.toDto(txn, transactionAttachService.getFirstImageName(txn)); + postResList.add(postRes); + } + + return postResList; + } + + public String toFormattedDate(LocalDateTime baseDateTime){ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + return baseDateTime.format(formatter); + } } diff --git a/src/main/java/HeyPorori/transaction/service/UserService.java b/src/main/java/HeyPorori/transaction/service/UserService.java new file mode 100644 index 0000000..3749c7e --- /dev/null +++ b/src/main/java/HeyPorori/transaction/service/UserService.java @@ -0,0 +1,51 @@ +package HeyPorori.transaction.service; + +import HeyPorori.transaction.config.BaseException; +import HeyPorori.transaction.config.BaseResponse; +import HeyPorori.transaction.config.BaseResponseStatus; +import HeyPorori.transaction.domain.UserInfo; +import HeyPorori.transaction.dto.UserInfoRes; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserService { + private final WebClient webClient; + private static final String LOG_FORMAT = "Method : {}"; + + public Long getUserId(String token) { + return postTokenRequest(token).getUserId(); + } + + public UserInfoRes getUserIdAndNickName(String token){ return UserInfoRes.toDto(postTokenRequest(token)); } + + private UserInfo postTokenRequest(String token) { + return webClient.get() + .uri("/token/me") + .header("Authorization", token) + .retrieve() + .bodyToMono((new ParameterizedTypeReference>() { + })) + .map(BaseResponse::getData) + .block(); + } + + public void sendTestJwtRequest(String token) { + try { + webClient.post() + .uri("/test/jwt") + .header("Authorization", token) + .retrieve() + .toBodilessEntity() + .block(); + } catch (Exception e) { + log.warn(LOG_FORMAT, "sendTestJwtRequest"); + throw new BaseException(BaseResponseStatus.INVALID_JWT); + } + } +}