diff --git a/.github/workflows/main-deploy.yml b/.github/workflows/main-deploy.yml index 46e4744f..83f2d813 100644 --- a/.github/workflows/main-deploy.yml +++ b/.github/workflows/main-deploy.yml @@ -44,7 +44,12 @@ jobs: jwt.secret-key: ${{ secrets.JWT_SECRET_KEY }} jwt.access-token.plus-hour: ${{ secrets.JWT_ACCESS_TOKEN_PLUS_HOUR }} jwt.refresh-token.plus-hour: ${{ secrets.JWT_REFRESH_TOKEN_PLUS_HOUR }} - servers.url: ${{ secrets.SERVERS_URL}} + servers.url: ${{ secrets.SERVERS_URL }} + cloud.aws.s3.bucket: ${{ secrets.CLOUD_AWS_S3_BUCKET }} + cloud.aws.credentials.access-key: ${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESS_KEY }} + cloud.aws.credentials.secret-key: ${{ secrets.CLOUD_AWS_CREDENTIALS_SECRET_KEY }} + cloud.aws.region.static: ${{ secrets.CLOUD_AWS_REGION_STATIC }} + - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/build.gradle b/build.gradle index a7b06686..7527fe96 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,9 @@ dependencies { //swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + //s3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' @@ -61,4 +64,4 @@ bootJar { jar { enabled = false -} \ No newline at end of file +} diff --git a/src/main/java/site/travellaboratory/be/common/exception/ErrorCodes.java b/src/main/java/site/travellaboratory/be/common/exception/ErrorCodes.java index a17c3ab4..c31ec085 100644 --- a/src/main/java/site/travellaboratory/be/common/exception/ErrorCodes.java +++ b/src/main/java/site/travellaboratory/be/common/exception/ErrorCodes.java @@ -33,12 +33,39 @@ public enum ErrorCodes { PASSWORD_INVALID_EMAIL("유효하지 않은 이메일", 2011L), PASSWORD_INQUIRY_INVALID_ANSWER("올바르지 않은 답변", 2012L), + // 유저 관련 + USER_NOT_FOUND("존재하지 않는 유저", 3000L), + // 후기 (review) - REVIEW_INVALID_ARTICLE("유효하지 않은 여행 계획 ID", 4000L), - REVIEW_NOT_USER_ARTICLE("여행 계획 작성자만 해당 여행 계획의 후기를 작성할 수 있습니다.", 4001L), - REVIEW_EXIST_USER_ARTICLE("각 여행 계획에 대한 후기는 한 개만 작성할 수 있습니다.", 4002L), + // 후기 작성 + REVIEW_POST_INVALID("[후기 작성] - 유효하지 않은 여행 계획 ID", 4000L), + REVIEW_POST_NOT_USER("[후기 작성] - 여행 계획 작성자만 해당 여행 계획의 후기를 작성할 수 있습니다.", 4001L), + REVIEW_POST_EXIST("[후기 작성] - 각 여행 계획에 대한 후기는 한 개만 작성할 수 있습니다.", 4002L), + // 후기 수정 + REVIEW_UPDATE_INVALID("[후기 수정] - 유효하지 않은 후기 ID", 4003L), + REVIEW_UPDATE_NOT_USER("[후기 수정] - 본인의 후기가 아닙니다.", 4004L), + // 후기 삭제 + REVIEW_DELETE_INVALID("[후기 삭제] - 유효하지 않은 후기 ID", 4005L), + REVIEW_DELETE_NOT_USER("[후기 삭제] - 본인의 후기가 아닙니다.", 4006L), + // 후기 좋아요 + REVIEW_LIKE_INVALID("[후기 좋아요] - 유효하지 않은 후기 ID", 4007L), + // 후기 상세 조회 + REVIEW_READ_DETAIL_INVALID("[후기 상세 조회] - 유효하지 않은 후기 ID", 4040L), + REVIEW_READ_DETAIL_NOT_AUTHORIZATION("[후기 상세 조회] - 해당 후기에 접근 권한 없음", 4041L), + + // 댓글 + // 댓글 작성 + COMMENT_POST_INVALID("[댓글 작성] - 유효하지 않은 후기 ID", 5000L), + // 댓글 수정 + COMMENT_UPDATE_INVALID("[댓글 수정] - 유효하지 않은 댓글 ID", 5010L), + COMMENT_UPDATE_NOT_USER("[댓글 수정] - 본인의 댓글이 아닙니다.", 5011L), + // 댓글 삭제 + COMMENT_DELETE_INVALID("[댓글 삭제] - 유효하지 않은 댓글 ID", 5020L), + COMMENT_DELETE_NOT_USER("[댓글 삭제] - 본인의 댓글가 아닙니다.", 5021L), + // 댓글 좋아요 + COMMENT_LIKE_INVALID("[댓글 좋아요] - 유효하지 않은 댓글 ID", 5030L), BAD_REQUEST("BAD_REQUEST", 9404L), BAD_REQUEST_JSON_PARSE_ERROR("[BAD_REQUEST] JSON_PARSE_ERROR - 올바른 JSON 형식이 아님", 9405L), diff --git a/src/main/java/site/travellaboratory/be/common/exceptionhandler/GlobalExceptionHandler.java b/src/main/java/site/travellaboratory/be/common/exceptionhandler/GlobalExceptionHandler.java index 0a0bd885..a4e155ad 100644 --- a/src/main/java/site/travellaboratory/be/common/exceptionhandler/GlobalExceptionHandler.java +++ b/src/main/java/site/travellaboratory/be/common/exceptionhandler/GlobalExceptionHandler.java @@ -4,7 +4,6 @@ import java.util.Map; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -17,7 +16,6 @@ import site.travellaboratory.be.common.exception.ErrorCodes; import site.travellaboratory.be.common.response.ApiErrorResponse; -@Profile("prod") @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { diff --git a/src/main/java/site/travellaboratory/be/config/S3Config.java b/src/main/java/site/travellaboratory/be/config/S3Config.java new file mode 100644 index 00000000..49d78132 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/config/S3Config.java @@ -0,0 +1,32 @@ +package site.travellaboratory.be.config; + +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() { + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} diff --git a/src/main/java/site/travellaboratory/be/controller/article/ArticleController.java b/src/main/java/site/travellaboratory/be/controller/article/ArticleController.java index 2f855d70..4ca8ee48 100644 --- a/src/main/java/site/travellaboratory/be/controller/article/ArticleController.java +++ b/src/main/java/site/travellaboratory/be/controller/article/ArticleController.java @@ -1,5 +1,6 @@ package site.travellaboratory.be.controller.article; +import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -13,6 +14,8 @@ import site.travellaboratory.be.common.annotation.UserId; import site.travellaboratory.be.controller.article.dto.ArticleRegisterRequest; import site.travellaboratory.be.controller.article.dto.ArticleResponse; +import site.travellaboratory.be.controller.article.dto.ArticleSearchRequest; +import site.travellaboratory.be.controller.article.dto.ArticleSearchResponse; import site.travellaboratory.be.service.ArticleService; @RestController @@ -44,4 +47,11 @@ public ResponseEntity findArticle( final ArticleResponse articleResponse = articleService.findByUserArticle(articleId); return ResponseEntity.ok(articleResponse); } + + @GetMapping("/article/search") + public ResponseEntity> searchArticle( + @Valid @RequestBody final ArticleSearchRequest articleSearchRequest) { + final List response = articleService.searchArticlesByKeyWord(articleSearchRequest); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/site/travellaboratory/be/controller/article/dto/ArticleSearchRequest.java b/src/main/java/site/travellaboratory/be/controller/article/dto/ArticleSearchRequest.java new file mode 100644 index 00000000..758399a0 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/article/dto/ArticleSearchRequest.java @@ -0,0 +1,9 @@ +package site.travellaboratory.be.controller.article.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ArticleSearchRequest ( + @NotBlank + String keyWord +){ +} diff --git a/src/main/java/site/travellaboratory/be/controller/article/dto/ArticleSearchResponse.java b/src/main/java/site/travellaboratory/be/controller/article/dto/ArticleSearchResponse.java new file mode 100644 index 00000000..40dc3a78 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/article/dto/ArticleSearchResponse.java @@ -0,0 +1,32 @@ +package site.travellaboratory.be.controller.article.dto; + +import java.time.LocalDateTime; +import java.util.List; +import site.travellaboratory.be.domain.article.Article; + +public record ArticleSearchResponse( + String title, + List location, + LocalDateTime startAt, + LocalDateTime endAt, + String expense, + List travelCompanion, + List style, + String nickname +) { + + public static List from(final List
articles) { + return articles.stream() + .map(article -> new ArticleSearchResponse( + article.getTitle(), + article.getLocation(), + article.getStartAt(), + article.getEndAt(), + article.getExpense(), + article.getTravelCompanions(), + article.getTravelStyles(), + article.getNickname() + )) + .toList(); + } +} diff --git a/src/main/java/site/travellaboratory/be/controller/comment/CommentController.java b/src/main/java/site/travellaboratory/be/controller/comment/CommentController.java new file mode 100644 index 00000000..fb9977d7 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/comment/CommentController.java @@ -0,0 +1,67 @@ +package site.travellaboratory.be.controller.comment; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import site.travellaboratory.be.common.annotation.UserId; +import site.travellaboratory.be.controller.comment.dto.CommentDeleteResponse; +import site.travellaboratory.be.controller.comment.dto.CommentSaveRequest; +import site.travellaboratory.be.controller.comment.dto.CommentSaveResponse; +import site.travellaboratory.be.controller.comment.dto.CommentUpdateRequest; +import site.travellaboratory.be.controller.comment.dto.CommentUpdateResponse; +import site.travellaboratory.be.controller.comment.dto.userlikecomment.CommentToggleLikeResponse; +import site.travellaboratory.be.service.CommentService; + +@RestController +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class CommentController { + + private final CommentService commentService; + + @PostMapping("/comment") + public ResponseEntity saveComment( + @UserId Long userId, + @Valid @RequestBody CommentSaveRequest commentSaveRequest + ) { + CommentSaveResponse response = commentService.saveComment(userId, commentSaveRequest); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @PatchMapping("/comments/{commentId}") + public ResponseEntity updateComment( + @UserId Long userId, + @PathVariable(name = "commentId") Long commentId, + @Valid @RequestBody CommentUpdateRequest commentUpdateRequest + ) { + CommentUpdateResponse response = commentService.updateComment(userId, commentId, commentUpdateRequest); + return ResponseEntity.ok(response); + } + + @PatchMapping("/comments/{commentId}/status") + public ResponseEntity deleteComment( + @UserId Long userId, + @PathVariable(name = "commentId") Long commentId + ) { + CommentDeleteResponse response = commentService.deleteComment(userId, commentId); + return ResponseEntity.ok(response); + } + + @PutMapping("/comments/{commentId}/likes") + public ResponseEntity toggleLikeComment( + @UserId Long userId, + @PathVariable(name = "commentId") Long commentId + ) { + CommentToggleLikeResponse response = commentService.toggleLikeComment(userId, + commentId); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentDeleteResponse.java b/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentDeleteResponse.java new file mode 100644 index 00000000..689965c2 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentDeleteResponse.java @@ -0,0 +1,11 @@ +package site.travellaboratory.be.controller.comment.dto; + +public record CommentDeleteResponse( + Boolean isDelete +) { + public static CommentDeleteResponse from(Boolean isDelete) { + return new CommentDeleteResponse( + isDelete + ); + } +} diff --git a/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentSaveRequest.java b/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentSaveRequest.java new file mode 100644 index 00000000..49ffd559 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentSaveRequest.java @@ -0,0 +1,12 @@ +package site.travellaboratory.be.controller.comment.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record CommentSaveRequest( + @NotNull + Long reviewId, + @NotBlank + String replyComment +) { +} diff --git a/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentSaveResponse.java b/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentSaveResponse.java new file mode 100644 index 00000000..48067c58 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentSaveResponse.java @@ -0,0 +1,9 @@ +package site.travellaboratory.be.controller.comment.dto; + +public record CommentSaveResponse( + Long commentId +) { + public static CommentSaveResponse from(Long commentId) { + return new CommentSaveResponse(commentId); + } +} diff --git a/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentUpdateRequest.java b/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentUpdateRequest.java new file mode 100644 index 00000000..0a91f2e6 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentUpdateRequest.java @@ -0,0 +1,7 @@ +package site.travellaboratory.be.controller.comment.dto; + +public record CommentUpdateRequest( + String replyComment +) { + +} diff --git a/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentUpdateResponse.java b/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentUpdateResponse.java new file mode 100644 index 00000000..074dc05f --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/comment/dto/CommentUpdateResponse.java @@ -0,0 +1,10 @@ +package site.travellaboratory.be.controller.comment.dto; + +public record CommentUpdateResponse( + Long commentId +) { + + public static CommentUpdateResponse from(Long commentId) { + return new CommentUpdateResponse(commentId); + } +} diff --git a/src/main/java/site/travellaboratory/be/controller/comment/dto/userlikecomment/CommentToggleLikeResponse.java b/src/main/java/site/travellaboratory/be/controller/comment/dto/userlikecomment/CommentToggleLikeResponse.java new file mode 100644 index 00000000..e58e09fa --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/comment/dto/userlikecomment/CommentToggleLikeResponse.java @@ -0,0 +1,11 @@ +package site.travellaboratory.be.controller.comment.dto.userlikecomment; + +import site.travellaboratory.be.domain.userlikecomment.UserLikeCommentStatus; + +public record CommentToggleLikeResponse( + UserLikeCommentStatus status +) { + public static CommentToggleLikeResponse from(UserLikeCommentStatus status) { + return new CommentToggleLikeResponse(status); + } +} diff --git a/src/main/java/site/travellaboratory/be/controller/review/ReviewController.java b/src/main/java/site/travellaboratory/be/controller/review/ReviewController.java index 295ee434..b2deb862 100644 --- a/src/main/java/site/travellaboratory/be/controller/review/ReviewController.java +++ b/src/main/java/site/travellaboratory/be/controller/review/ReviewController.java @@ -4,14 +4,22 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import site.travellaboratory.be.common.annotation.UserId; +import site.travellaboratory.be.controller.review.dto.ReviewDeleteResponse; +import site.travellaboratory.be.controller.review.dto.ReviewReadDetailResponse; import site.travellaboratory.be.controller.review.dto.ReviewSaveRequest; import site.travellaboratory.be.controller.review.dto.ReviewSaveResponse; +import site.travellaboratory.be.controller.review.dto.ReviewUpdateRequest; +import site.travellaboratory.be.controller.review.dto.ReviewUpdateResponse; +import site.travellaboratory.be.controller.review.dto.userlikereview.ReviewToggleLikeResponse; import site.travellaboratory.be.service.ReviewService; @RestController @@ -21,14 +29,51 @@ public class ReviewController { private final ReviewService reviewService; - @PostMapping("/articles/{articleId}/review") + @GetMapping("/reviews/{reviewId}") + public ResponseEntity readReviewDetail( + @UserId Long userId, + @PathVariable(name = "reviewId") Long reviewId + ) { + ReviewReadDetailResponse response = reviewService.readReviewDetail(userId, + reviewId); + return ResponseEntity.ok(response); + } + + @PostMapping("/review") public ResponseEntity saveReview( @UserId Long userId, - @PathVariable(name = "articleId") Long articleId, @Valid @RequestBody ReviewSaveRequest reviewSaveRequest ) { - ReviewSaveResponse response = reviewService.saveReview(userId, articleId, - reviewSaveRequest); + ReviewSaveResponse response = reviewService.saveReview(userId, reviewSaveRequest); return new ResponseEntity<>(response, HttpStatus.CREATED); } + + @PatchMapping("/reviews/{reviewId}") + public ResponseEntity updateReview( + @UserId Long userId, + @PathVariable(name = "reviewId") Long reviewId, + @Valid @RequestBody ReviewUpdateRequest reviewUpdateRequest + ) { + ReviewUpdateResponse response = reviewService.updateReview(userId, reviewId, reviewUpdateRequest); + return ResponseEntity.ok(response); + } + + @PatchMapping("/reviews/{reviewId}/status") + public ResponseEntity deleteReview( + @UserId Long userId, + @PathVariable(name = "reviewId") Long reviewId + ) { + ReviewDeleteResponse response = reviewService.deleteReview(userId, reviewId); + return ResponseEntity.ok(response); + } + + @PutMapping("/reviews/{reviewId}/likes") + public ResponseEntity toggleLikeReview( + @UserId Long userId, + @PathVariable(name = "reviewId") Long reviewId + ) { + ReviewToggleLikeResponse response = reviewService.toggleLikeReview(userId, + reviewId); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewDeleteResponse.java b/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewDeleteResponse.java new file mode 100644 index 00000000..5556282a --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewDeleteResponse.java @@ -0,0 +1,12 @@ +package site.travellaboratory.be.controller.review.dto; + +public record ReviewDeleteResponse( + Boolean isDelete +) { + public static ReviewDeleteResponse from(Boolean isDelete) { + return new ReviewDeleteResponse( + isDelete + ); + } +} + diff --git a/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewReadDetailResponse.java b/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewReadDetailResponse.java new file mode 100644 index 00000000..b033d1fa --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewReadDetailResponse.java @@ -0,0 +1,38 @@ +package site.travellaboratory.be.controller.review.dto; + +import java.time.format.DateTimeFormatter; +import site.travellaboratory.be.domain.review.Review; + +public record ReviewReadDetailResponse( + Long userId, + String profileImgUrl, + String nickname, + boolean isEditable, + Long articleId, + Long reviewId, + String title, + String representativeImgUrl, + String description, + String createdAt, + boolean isLike, + long likeCount +) { + public static ReviewReadDetailResponse from(Review review, + boolean isEditable,boolean isLike,long likeCount + ) { + return new ReviewReadDetailResponse( + review.getUser().getId(), + review.getUser().getProfileImgUrl(), + review.getUser().getNickname(), + isEditable, + review.getArticle().getId(), + review.getId(), + review.getTitle(), + review.getRepresentativeImgUrl(), + review.getDescription(), + review.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), + isLike, + likeCount + ); + } +} diff --git a/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewSaveRequest.java b/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewSaveRequest.java index 0b91a5ee..73b21a47 100644 --- a/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewSaveRequest.java +++ b/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewSaveRequest.java @@ -1,8 +1,11 @@ package site.travellaboratory.be.controller.review.dto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public record ReviewSaveRequest( + @NotNull + Long articleId, @NotBlank String title, String representativeImgUrl, diff --git a/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewUpdateRequest.java b/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewUpdateRequest.java new file mode 100644 index 00000000..d8cf8b67 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewUpdateRequest.java @@ -0,0 +1,13 @@ +package site.travellaboratory.be.controller.review.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ReviewUpdateRequest( + @NotBlank + String title, + String representativeImgUrl, + @NotBlank + String description +) { + +} diff --git a/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewUpdateResponse.java b/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewUpdateResponse.java new file mode 100644 index 00000000..085567dd --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/review/dto/ReviewUpdateResponse.java @@ -0,0 +1,11 @@ +package site.travellaboratory.be.controller.review.dto; + +public record ReviewUpdateResponse( + Long reviewId +) { + public static ReviewUpdateResponse from(Long reviewId) { + return new ReviewUpdateResponse( + reviewId + ); + } +} diff --git a/src/main/java/site/travellaboratory/be/controller/review/dto/userlikereview/ReviewToggleLikeResponse.java b/src/main/java/site/travellaboratory/be/controller/review/dto/userlikereview/ReviewToggleLikeResponse.java new file mode 100644 index 00000000..b4816775 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/controller/review/dto/userlikereview/ReviewToggleLikeResponse.java @@ -0,0 +1,11 @@ +package site.travellaboratory.be.controller.review.dto.userlikereview; + +import site.travellaboratory.be.domain.userlikereview.UserLikeReviewStatus; + +public record ReviewToggleLikeResponse( + UserLikeReviewStatus status +) { + public static ReviewToggleLikeResponse from(UserLikeReviewStatus status) { + return new ReviewToggleLikeResponse(status); + } +} diff --git a/src/main/java/site/travellaboratory/be/controller/user/UserController.java b/src/main/java/site/travellaboratory/be/controller/user/UserController.java index eaec756b..6c040335 100644 --- a/src/main/java/site/travellaboratory/be/controller/user/UserController.java +++ b/src/main/java/site/travellaboratory/be/controller/user/UserController.java @@ -4,15 +4,12 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import site.travellaboratory.be.common.annotation.UserId; -import site.travellaboratory.be.controller.user.dto.ProfileImgUpdateRequest; -import site.travellaboratory.be.controller.user.dto.ProfileImgUpdateResponse; import site.travellaboratory.be.controller.user.dto.UserProfileResponse; import site.travellaboratory.be.controller.user.dto.UserProfileUpdateRequest; import site.travellaboratory.be.controller.user.dto.UserProfileUpdateResponse; @@ -33,20 +30,12 @@ public ResponseEntity findMyProfile(@UserId final Long user @PutMapping("/profile") public ResponseEntity updateMyProfile( - @RequestBody UserProfileUpdateRequest userProfileUpdateRequest, + @RequestPart("file") final MultipartFile file, + @RequestPart("profile") final UserProfileUpdateRequest userProfileUpdateRequest, @UserId final Long userId) { - final UserProfileUpdateResponse userProfileUpdateResponse = userService.updateProfile(userProfileUpdateRequest, + final UserProfileUpdateResponse userProfileUpdateResponse = userService.updateProfile(file, + userProfileUpdateRequest, userId); return ResponseEntity.status(HttpStatus.ACCEPTED).body(userProfileUpdateResponse); } - - @PutMapping("/profile/image") - public ResponseEntity updateMyProfileImage( - @RequestBody ProfileImgUpdateRequest profileImgUpdateRequest, - @UserId final Long userId - ) { - final ProfileImgUpdateResponse profileImgUpdateResponse = userService.updateProfileImage( - profileImgUpdateRequest, userId); - return ResponseEntity.status(HttpStatus.ACCEPTED).body(profileImgUpdateResponse); - } } diff --git a/src/main/java/site/travellaboratory/be/controller/user/dto/ProfileImgUpdateRequest.java b/src/main/java/site/travellaboratory/be/controller/user/dto/ProfileImgUpdateRequest.java deleted file mode 100644 index 1610ba1e..00000000 --- a/src/main/java/site/travellaboratory/be/controller/user/dto/ProfileImgUpdateRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package site.travellaboratory.be.controller.user.dto; - -public record ProfileImgUpdateRequest ( - String profileImgUrl -){ -} diff --git a/src/main/java/site/travellaboratory/be/controller/user/dto/ProfileImgUpdateResponse.java b/src/main/java/site/travellaboratory/be/controller/user/dto/ProfileImgUpdateResponse.java deleted file mode 100644 index 12500d01..00000000 --- a/src/main/java/site/travellaboratory/be/controller/user/dto/ProfileImgUpdateResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package site.travellaboratory.be.controller.user.dto; - -public record ProfileImgUpdateResponse ( - String profileImgUrl -){ -} - diff --git a/src/main/java/site/travellaboratory/be/controller/user/dto/UserProfileUpdateRequest.java b/src/main/java/site/travellaboratory/be/controller/user/dto/UserProfileUpdateRequest.java index 23d0f44c..88f1ea70 100644 --- a/src/main/java/site/travellaboratory/be/controller/user/dto/UserProfileUpdateRequest.java +++ b/src/main/java/site/travellaboratory/be/controller/user/dto/UserProfileUpdateRequest.java @@ -1,7 +1,8 @@ package site.travellaboratory.be.controller.user.dto; -public record UserProfileUpdateRequest ( +public record UserProfileUpdateRequest( + String username, String nickname, String introduce -){ +) { } diff --git a/src/main/java/site/travellaboratory/be/controller/user/dto/UserProfileUpdateResponse.java b/src/main/java/site/travellaboratory/be/controller/user/dto/UserProfileUpdateResponse.java index be77802e..f2250bb9 100644 --- a/src/main/java/site/travellaboratory/be/controller/user/dto/UserProfileUpdateResponse.java +++ b/src/main/java/site/travellaboratory/be/controller/user/dto/UserProfileUpdateResponse.java @@ -1,7 +1,9 @@ package site.travellaboratory.be.controller.user.dto; -public record UserProfileUpdateResponse ( +public record UserProfileUpdateResponse( + String username, String nickname, + String profileImgUrl, String introduce -){ +) { } diff --git a/src/main/java/site/travellaboratory/be/domain/article/Article.java b/src/main/java/site/travellaboratory/be/domain/article/Article.java index d697c0e5..a05920bb 100644 --- a/src/main/java/site/travellaboratory/be/domain/article/Article.java +++ b/src/main/java/site/travellaboratory/be/domain/article/Article.java @@ -38,7 +38,6 @@ public class Article extends BaseEntity { @ElementCollection @CollectionTable(name = "article_locations", joinColumns = @JoinColumn(name = "article_id")) - @Column(name = "location") private List location = new ArrayList<>(); private Duration duration; diff --git a/src/main/java/site/travellaboratory/be/domain/article/ArticleRepository.java b/src/main/java/site/travellaboratory/be/domain/article/ArticleRepository.java index 9d07f265..b8eb0302 100644 --- a/src/main/java/site/travellaboratory/be/domain/article/ArticleRepository.java +++ b/src/main/java/site/travellaboratory/be/domain/article/ArticleRepository.java @@ -3,6 +3,8 @@ import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import site.travellaboratory.be.domain.user.entity.User; @@ -15,4 +17,7 @@ default Article getById(final Long id) { List
findArticlesByUser(final User user); Optional
findByIdAndStatusIn(final Long articleId, List Status); + + @Query("SELECT a FROM Article a JOIN a.location l WHERE l = :keyWord AND a.status = site.travellaboratory.be.domain.article.ArticleStatus.ACTIVE") + List
findByLocationContainingAndStatusActive(@Param("keyWord") String keyWord); } diff --git a/src/main/java/site/travellaboratory/be/domain/comment/Comment.java b/src/main/java/site/travellaboratory/be/domain/comment/Comment.java new file mode 100644 index 00000000..1c6f85b0 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/domain/comment/Comment.java @@ -0,0 +1,69 @@ +package site.travellaboratory.be.domain.comment; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import site.travellaboratory.be.domain.BaseEntity; +import site.travellaboratory.be.domain.review.Review; +import site.travellaboratory.be.domain.user.entity.User; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Comment extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id", nullable = false) + private Review review; + + @Column(nullable = false, columnDefinition = "TEXT") + private String replyContent; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private CommentStatus status; + + private Comment( + User user, + Review review, + String replyContent + ) { + this.user = user; + this.review = review; + this.replyContent = replyContent; + this.status = CommentStatus.ACTIVE; + } + + public static Comment of( + User user, + Review review, + String replyContent + ) { + return new Comment(user, review, replyContent); + } + + public void update(String replyContent) { + this.replyContent = replyContent; + } + + public void delete() { + this.status = CommentStatus.INACTIVE; + } +} diff --git a/src/main/java/site/travellaboratory/be/domain/comment/CommentRepository.java b/src/main/java/site/travellaboratory/be/domain/comment/CommentRepository.java new file mode 100644 index 00000000..602f7a5e --- /dev/null +++ b/src/main/java/site/travellaboratory/be/domain/comment/CommentRepository.java @@ -0,0 +1,10 @@ +package site.travellaboratory.be.domain.comment; + +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { + Optional findByIdAndStatusIn(Long commentId, List status); + +} diff --git a/src/main/java/site/travellaboratory/be/domain/comment/CommentStatus.java b/src/main/java/site/travellaboratory/be/domain/comment/CommentStatus.java new file mode 100644 index 00000000..6ca4d7e2 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/domain/comment/CommentStatus.java @@ -0,0 +1,6 @@ +package site.travellaboratory.be.domain.comment; + +public enum CommentStatus { + ACTIVE, + INACTIVE +} diff --git a/src/main/java/site/travellaboratory/be/domain/review/Review.java b/src/main/java/site/travellaboratory/be/domain/review/Review.java index d5db0472..018a69bf 100644 --- a/src/main/java/site/travellaboratory/be/domain/review/Review.java +++ b/src/main/java/site/travellaboratory/be/domain/review/Review.java @@ -53,6 +53,7 @@ public class Review extends BaseEntity { @Column(nullable = false) private ReviewStatus status; + // todo: private 으로 변경 public Review( User user, Article article, @@ -77,4 +78,14 @@ public static Review of( ) { return new Review(user, article, title, representativeImgUrl, description); } + + public void update(String title, String representativeImgUrl, String description) { + this.title = title; + this.representativeImgUrl = representativeImgUrl; + this.description = description; + } + + public void delete() { + this.status = ReviewStatus.INACTIVE; + } } diff --git a/src/main/java/site/travellaboratory/be/domain/review/ReviewRepository.java b/src/main/java/site/travellaboratory/be/domain/review/ReviewRepository.java index cb47a425..2a2d8970 100644 --- a/src/main/java/site/travellaboratory/be/domain/review/ReviewRepository.java +++ b/src/main/java/site/travellaboratory/be/domain/review/ReviewRepository.java @@ -1,5 +1,6 @@ package site.travellaboratory.be.domain.review; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import site.travellaboratory.be.domain.article.Article; @@ -7,4 +8,6 @@ public interface ReviewRepository extends JpaRepository { Optional findByArticleAndStatusNotOrderByArticleDesc(Article article, ReviewStatus status); + + Optional findByIdAndStatusIn(Long reviewId, List status); } diff --git a/src/main/java/site/travellaboratory/be/domain/user/entity/User.java b/src/main/java/site/travellaboratory/be/domain/user/entity/User.java index fcc74e4a..d21376c7 100644 --- a/src/main/java/site/travellaboratory/be/domain/user/entity/User.java +++ b/src/main/java/site/travellaboratory/be/domain/user/entity/User.java @@ -44,34 +44,30 @@ public class User extends BaseEntity { // @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) // private PwAnswer pwAnswer; - public User(Long id, String username) { - this.id = id; - this.username = username; - } - @PrePersist void prePersist() { this.role = UserRole.USER; this.status = UserStatus.ACTIVE; } - public static User of(Long id, String username) { - return new User(id, username); - } - - public User(Long id, String nickname, String introduce) { + // 프로필 변경 + public User(Long id, String username, String nickname, String profileImgUrl, String introduce) { this.id = id; + this.username = username; this.nickname = nickname; + this.profileImgUrl = profileImgUrl; this.introduce = introduce; + this.status = UserStatus.ACTIVE; } - public User update(String nickname, String introduce) { - return new User(this.id, nickname, introduce); + // 프로필 변경 + public User update(String username, String nickname, String profileImgUrl, String introduce) { + return new User(this.id, username, nickname, profileImgUrl, introduce); } - public User updateProfileImg(String profileImgUrl) { - return new User(this.id, nickname, profileImgUrl); - } +// public User updateProfileImg(String profileImgUrl) { +// return new User(this.id, nickname, profileImgUrl); +// } public void setPassword(final String password) { this.password = password; @@ -97,4 +93,15 @@ public static User of(String username, String password, String nickname, Boolean user.setIsAgreement(isAgreement); return user; } + + // 후기 좋아요를 위한 생성자 + private User(Long id) { + this.id = id; + } + + // 후기 좋아요를 위한 of + public static User of(Long userId) { + return new User(userId); + } + } diff --git a/src/main/java/site/travellaboratory/be/domain/userlikecomment/UserLikeComment.java b/src/main/java/site/travellaboratory/be/domain/userlikecomment/UserLikeComment.java new file mode 100644 index 00000000..89cb4d29 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/domain/userlikecomment/UserLikeComment.java @@ -0,0 +1,57 @@ +package site.travellaboratory.be.domain.userlikecomment; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import site.travellaboratory.be.domain.BaseEntity; +import site.travellaboratory.be.domain.comment.Comment; +import site.travellaboratory.be.domain.user.entity.User; + +@Entity +@Getter +@NoArgsConstructor +public class UserLikeComment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id", nullable = false) + private Comment comment; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private UserLikeCommentStatus status; + + private UserLikeComment(User user, Comment comment) { + this.user = user; + this.comment = comment; + this.status = UserLikeCommentStatus.ACTIVE; + } + + public static UserLikeComment of(User user, Comment comment) { + return new UserLikeComment(user, comment); + } + + public void toggleStatus() { + if (this.status == UserLikeCommentStatus.ACTIVE) { + this.status = UserLikeCommentStatus.INACTIVE; + } else { + this.status = UserLikeCommentStatus.ACTIVE; + } + } +} \ No newline at end of file diff --git a/src/main/java/site/travellaboratory/be/domain/userlikecomment/UserLikeCommentRepository.java b/src/main/java/site/travellaboratory/be/domain/userlikecomment/UserLikeCommentRepository.java new file mode 100644 index 00000000..26634b22 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/domain/userlikecomment/UserLikeCommentRepository.java @@ -0,0 +1,12 @@ +package site.travellaboratory.be.domain.userlikecomment; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface UserLikeCommentRepository extends JpaRepository { + + @Query("SELECT t FROM UserLikeComment t WHERE t.user.id = :userId AND t.comment.id = :commentId") + Optional findByUserIdAndCommentId(@Param("userId") Long userId, @Param("commentId") Long commentId); +} diff --git a/src/main/java/site/travellaboratory/be/domain/userlikecomment/UserLikeCommentStatus.java b/src/main/java/site/travellaboratory/be/domain/userlikecomment/UserLikeCommentStatus.java new file mode 100644 index 00000000..b4f20838 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/domain/userlikecomment/UserLikeCommentStatus.java @@ -0,0 +1,6 @@ +package site.travellaboratory.be.domain.userlikecomment; + +public enum UserLikeCommentStatus { + ACTIVE, + INACTIVE +} \ No newline at end of file diff --git a/src/main/java/site/travellaboratory/be/domain/userslikerereview/UserLikeReview.java b/src/main/java/site/travellaboratory/be/domain/userlikereview/UserLikeReview.java similarity index 65% rename from src/main/java/site/travellaboratory/be/domain/userslikerereview/UserLikeReview.java rename to src/main/java/site/travellaboratory/be/domain/userlikereview/UserLikeReview.java index 95fc7b38..317f9e76 100644 --- a/src/main/java/site/travellaboratory/be/domain/userslikerereview/UserLikeReview.java +++ b/src/main/java/site/travellaboratory/be/domain/userlikereview/UserLikeReview.java @@ -1,4 +1,4 @@ -package site.travellaboratory.be.domain.userslikerereview; +package site.travellaboratory.be.domain.userlikereview; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -36,4 +36,22 @@ public class UserLikeReview extends BaseEntity { @Enumerated(EnumType.STRING) @Column(nullable = false) private UserLikeReviewStatus status; + + public static UserLikeReview of(User user, Review review) { + return new UserLikeReview(user, review); + } + + private UserLikeReview(User user, Review review) { + this.user = user; + this.review = review; + this.status = UserLikeReviewStatus.ACTIVE; + } + + public void toggleStatus() { + if (this.status == UserLikeReviewStatus.ACTIVE) { + this.status = UserLikeReviewStatus.INACTIVE; + } else { + this.status = UserLikeReviewStatus.ACTIVE; + } + } } diff --git a/src/main/java/site/travellaboratory/be/domain/userlikereview/UserLikeReviewRepository.java b/src/main/java/site/travellaboratory/be/domain/userlikereview/UserLikeReviewRepository.java new file mode 100644 index 00000000..71b5bcd0 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/domain/userlikereview/UserLikeReviewRepository.java @@ -0,0 +1,15 @@ +package site.travellaboratory.be.domain.userlikereview; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface UserLikeReviewRepository extends JpaRepository { + + @Query("SELECT t FROM UserLikeReview t WHERE t.user.id = :userId AND t.review.id = :reviewId") + Optional findByUserIdAndReviewId(@Param("userId") Long userId, @Param("reviewId") Long reviewId); + + // 후기 상세 조회 - 좋아요 개수 + Long countByReviewIdAndStatus(Long reviewId, UserLikeReviewStatus status); +} diff --git a/src/main/java/site/travellaboratory/be/domain/userslikerereview/UserLikeReviewStatus.java b/src/main/java/site/travellaboratory/be/domain/userlikereview/UserLikeReviewStatus.java similarity index 51% rename from src/main/java/site/travellaboratory/be/domain/userslikerereview/UserLikeReviewStatus.java rename to src/main/java/site/travellaboratory/be/domain/userlikereview/UserLikeReviewStatus.java index ef37f24d..6413c491 100644 --- a/src/main/java/site/travellaboratory/be/domain/userslikerereview/UserLikeReviewStatus.java +++ b/src/main/java/site/travellaboratory/be/domain/userlikereview/UserLikeReviewStatus.java @@ -1,4 +1,4 @@ -package site.travellaboratory.be.domain.userslikerereview; +package site.travellaboratory.be.domain.userlikereview; public enum UserLikeReviewStatus { ACTIVE, diff --git a/src/main/java/site/travellaboratory/be/service/ArticleService.java b/src/main/java/site/travellaboratory/be/service/ArticleService.java index 9dc318f6..240a85d5 100644 --- a/src/main/java/site/travellaboratory/be/service/ArticleService.java +++ b/src/main/java/site/travellaboratory/be/service/ArticleService.java @@ -5,6 +5,8 @@ import org.springframework.stereotype.Service; import site.travellaboratory.be.controller.article.dto.ArticleRegisterRequest; import site.travellaboratory.be.controller.article.dto.ArticleResponse; +import site.travellaboratory.be.controller.article.dto.ArticleSearchRequest; +import site.travellaboratory.be.controller.article.dto.ArticleSearchResponse; import site.travellaboratory.be.domain.article.Article; import site.travellaboratory.be.domain.article.ArticleRepository; import site.travellaboratory.be.domain.user.entity.User; @@ -34,5 +36,11 @@ public ArticleResponse findByUserArticle(final Long articleId) { final Article article = articleRepository.getById(articleId); return ArticleResponse.from(article); } + + public List searchArticlesByKeyWord(final ArticleSearchRequest articleSearchRequest) { + final List
articles = articleRepository.findByLocationContainingAndStatusActive( + articleSearchRequest.keyWord()); + return ArticleSearchResponse.from(articles); + } } diff --git a/src/main/java/site/travellaboratory/be/service/CommentService.java b/src/main/java/site/travellaboratory/be/service/CommentService.java new file mode 100644 index 00000000..58179dd0 --- /dev/null +++ b/src/main/java/site/travellaboratory/be/service/CommentService.java @@ -0,0 +1,112 @@ +package site.travellaboratory.be.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import site.travellaboratory.be.common.exception.BeApplicationException; +import site.travellaboratory.be.common.exception.ErrorCodes; +import site.travellaboratory.be.controller.comment.dto.CommentDeleteResponse; +import site.travellaboratory.be.controller.comment.dto.CommentSaveRequest; +import site.travellaboratory.be.controller.comment.dto.CommentSaveResponse; +import site.travellaboratory.be.controller.comment.dto.userlikecomment.CommentToggleLikeResponse; +import site.travellaboratory.be.controller.comment.dto.CommentUpdateRequest; +import site.travellaboratory.be.controller.comment.dto.CommentUpdateResponse; +import site.travellaboratory.be.domain.comment.Comment; +import site.travellaboratory.be.domain.comment.CommentRepository; +import site.travellaboratory.be.domain.comment.CommentStatus; +import site.travellaboratory.be.domain.review.Review; +import site.travellaboratory.be.domain.review.ReviewRepository; +import site.travellaboratory.be.domain.review.ReviewStatus; +import site.travellaboratory.be.domain.user.entity.User; +import site.travellaboratory.be.domain.userlikecomment.UserLikeComment; +import site.travellaboratory.be.domain.userlikecomment.UserLikeCommentRepository; + +@Service +@RequiredArgsConstructor +public class CommentService { + + private final ReviewRepository reviewRepository; + private final CommentRepository commentRepository; + private final UserLikeCommentRepository userLikeCommentRepository; + + @Transactional + public CommentSaveResponse saveComment(Long userId, CommentSaveRequest request) { + // 삭제된 후기에 대한 댓글을 작성할 경우 + Review review = reviewRepository.findByIdAndStatusIn(request.reviewId(), + List.of(ReviewStatus.ACTIVE, ReviewStatus.PRIVATE)) + .orElseThrow(() -> new BeApplicationException(ErrorCodes.COMMENT_POST_INVALID, + HttpStatus.NOT_FOUND)); + + + // 댓글 작성 + Comment saveComment = commentRepository.save( + Comment.of( + review.getUser(), + review, + request.replyComment() + ) + ); + return CommentSaveResponse.from(saveComment.getId()); + } + + @Transactional + public CommentUpdateResponse updateComment(Long userId, Long commentId, CommentUpdateRequest request) { + // 삭제된 댓글를 수정할 경우 + Comment comment = commentRepository.findByIdAndStatusIn(commentId, List.of(CommentStatus.ACTIVE)) + .orElseThrow(() -> new BeApplicationException(ErrorCodes.COMMENT_UPDATE_INVALID, + HttpStatus.NOT_FOUND)); + + // 유저가 작성한 댓글이 아닌 경우 + if (!comment.getUser().getId().equals(userId)) { + throw new BeApplicationException(ErrorCodes.COMMENT_UPDATE_NOT_USER, HttpStatus.FORBIDDEN); + } + + // 댓글 업데이트 + comment.update(request.replyComment()); + Comment updateComment = commentRepository.save(comment); + return CommentUpdateResponse.from(updateComment.getId()); + } + + @Transactional + public CommentDeleteResponse deleteComment(final Long userId,final Long commentId) { + // 삭제된 댓글을 삭제할 경우 + Comment comment = commentRepository.findByIdAndStatusIn(commentId, List.of(CommentStatus.ACTIVE)) + .orElseThrow(() -> new BeApplicationException(ErrorCodes.COMMENT_DELETE_INVALID, + HttpStatus.NOT_FOUND)); + + // 유저가 작성한 댓글이 아닌 경우 + if (!comment.getUser().getId().equals(userId)) { + throw new BeApplicationException(ErrorCodes.COMMENT_DELETE_NOT_USER, HttpStatus.FORBIDDEN); + } + + // 댓글 삭제 + comment.delete(); + commentRepository.save(comment); + return CommentDeleteResponse.from(true); + } + + @Transactional + public CommentToggleLikeResponse toggleLikeComment(Long userId, Long commentId) { + // 삭제된 댓글에 좋아요하려고 할 경우 + Comment comment = commentRepository.findByIdAndStatusIn(commentId, List.of(CommentStatus.ACTIVE)) + .orElseThrow(() -> new BeApplicationException(ErrorCodes.COMMENT_LIKE_INVALID, + HttpStatus.NOT_FOUND)); + + UserLikeComment userLikeComment = userLikeCommentRepository.findByUserIdAndCommentId(userId, commentId) + .orElse(null); + + // 댓글에 처음 좋아요를 누른 게 아닌 경우 + if (userLikeComment != null) { + userLikeComment.toggleStatus(); + } else { + // 댓글에 처음 좋아요를 누른 경우 - 새로 생성 + User user = User.of(userId); + userLikeComment = UserLikeComment.of(user, comment); + } + + UserLikeComment saveLikeComment = userLikeCommentRepository.save(userLikeComment); + return CommentToggleLikeResponse.from(saveLikeComment.getStatus()); + } +} diff --git a/src/main/java/site/travellaboratory/be/service/ReviewService.java b/src/main/java/site/travellaboratory/be/service/ReviewService.java index 909c414e..8a15fcd2 100644 --- a/src/main/java/site/travellaboratory/be/service/ReviewService.java +++ b/src/main/java/site/travellaboratory/be/service/ReviewService.java @@ -7,14 +7,23 @@ import org.springframework.transaction.annotation.Transactional; import site.travellaboratory.be.common.exception.BeApplicationException; import site.travellaboratory.be.common.exception.ErrorCodes; +import site.travellaboratory.be.controller.review.dto.ReviewDeleteResponse; +import site.travellaboratory.be.controller.review.dto.ReviewReadDetailResponse; import site.travellaboratory.be.controller.review.dto.ReviewSaveRequest; import site.travellaboratory.be.controller.review.dto.ReviewSaveResponse; +import site.travellaboratory.be.controller.review.dto.ReviewUpdateRequest; +import site.travellaboratory.be.controller.review.dto.ReviewUpdateResponse; +import site.travellaboratory.be.controller.review.dto.userlikereview.ReviewToggleLikeResponse; import site.travellaboratory.be.domain.article.Article; import site.travellaboratory.be.domain.article.ArticleRepository; import site.travellaboratory.be.domain.article.ArticleStatus; import site.travellaboratory.be.domain.review.Review; import site.travellaboratory.be.domain.review.ReviewRepository; import site.travellaboratory.be.domain.review.ReviewStatus; +import site.travellaboratory.be.domain.user.entity.User; +import site.travellaboratory.be.domain.userlikereview.UserLikeReview; +import site.travellaboratory.be.domain.userlikereview.UserLikeReviewRepository; +import site.travellaboratory.be.domain.userlikereview.UserLikeReviewStatus; @Service @RequiredArgsConstructor @@ -22,28 +31,61 @@ public class ReviewService { private final ReviewRepository reviewRepository; private final ArticleRepository articleRepository; + private final UserLikeReviewRepository userLikeReviewRepository; + + public ReviewReadDetailResponse readReviewDetail(Long userId, Long reviewId) { + // 유효하지 않은 후기를 조회할 경우 + Review review = reviewRepository.findByIdAndStatusIn(reviewId, + List.of(ReviewStatus.ACTIVE, ReviewStatus.PRIVATE)) + .orElseThrow(() -> new BeApplicationException(ErrorCodes.REVIEW_READ_DETAIL_INVALID, + HttpStatus.NOT_FOUND)); + + // 나만보기 상태의 글을 글쓴이가 아닌 다른 유저가 조회할 경우 + if (review.getStatus() == ReviewStatus.PRIVATE && (!review.getUser().getId() + .equals(userId))) { + throw new BeApplicationException(ErrorCodes.REVIEW_READ_DETAIL_NOT_AUTHORIZATION, + HttpStatus.FORBIDDEN); + } + + // 후기 조회 + // (1) 수정, 삭제 권한 + boolean isEditable = review.getUser().getId().equals(userId); + + // (2) 좋아요 + boolean isLike = userLikeReviewRepository.findByUserIdAndReviewId(userId, reviewId) + .map(UserLikeReview::getStatus) + .orElse(UserLikeReviewStatus.INACTIVE) == UserLikeReviewStatus.ACTIVE; + + // (3) 좋아요 개수 + long likeCount = userLikeReviewRepository.countByReviewIdAndStatus(reviewId, + UserLikeReviewStatus.ACTIVE); + + return ReviewReadDetailResponse.from( + review, isEditable, isLike, likeCount + ); + } @Transactional - public ReviewSaveResponse saveReview(Long userId, Long articleId, ReviewSaveRequest request) { + public ReviewSaveResponse saveReview(Long userId, ReviewSaveRequest request) { // 삭제된 여행 계획에 대한 후기를 작성할 경우 - Article article = articleRepository.findByIdAndStatusIn(articleId, List.of(ArticleStatus.ACTIVE, ArticleStatus.PRIVATE)) - .orElseThrow(() -> new BeApplicationException(ErrorCodes.REVIEW_INVALID_ARTICLE, + Article article = articleRepository.findByIdAndStatusIn(request.articleId(), List.of(ArticleStatus.ACTIVE, ArticleStatus.PRIVATE)) + .orElseThrow(() -> new BeApplicationException(ErrorCodes.REVIEW_POST_INVALID, HttpStatus.NOT_FOUND)); // 유저가 작성한 article_id이 아닌 경우 if (!article.getUser().getId().equals(userId)) { - throw new BeApplicationException(ErrorCodes.REVIEW_NOT_USER_ARTICLE, HttpStatus.FORBIDDEN); + throw new BeApplicationException(ErrorCodes.REVIEW_POST_NOT_USER, HttpStatus.FORBIDDEN); } // 이미 해당 여행 계획에 대한 후기가 있을 경우 reviewRepository.findByArticleAndStatusNotOrderByArticleDesc(article, ReviewStatus.INACTIVE) .ifPresent(it -> { - throw new BeApplicationException(ErrorCodes.REVIEW_EXIST_USER_ARTICLE, + throw new BeApplicationException(ErrorCodes.REVIEW_POST_EXIST, HttpStatus.CONFLICT); }); // 본인이 작성한 여행 계획 + 후기가 없는 경우 - Review saveEntity = reviewRepository.save( + Review saveReview = reviewRepository.save( Review.of( article.getUser(), article, @@ -52,6 +94,69 @@ public ReviewSaveResponse saveReview(Long userId, Long articleId, ReviewSaveRequ request.description() ) ); - return ReviewSaveResponse.from(saveEntity.getId()); + return ReviewSaveResponse.from(saveReview.getId()); + } + + @Transactional + public ReviewUpdateResponse updateReview(Long userId, Long reviewId, ReviewUpdateRequest request) { + // 삭제된 후기를 수정할 경우 + Review review = reviewRepository.findByIdAndStatusIn(reviewId, + List.of(ReviewStatus.ACTIVE, ReviewStatus.PRIVATE)) + .orElseThrow(() -> new BeApplicationException(ErrorCodes.REVIEW_UPDATE_INVALID, + HttpStatus.NOT_FOUND)); + + // 유저가 작성한 후기가 아닌 경우 + if (!review.getUser().getId().equals(userId)) { + throw new BeApplicationException(ErrorCodes.REVIEW_UPDATE_NOT_USER, HttpStatus.FORBIDDEN); + } + + // 후기 업데이트 + review.update(request.title(), request.representativeImgUrl(), request.description()); + Review updateReview = reviewRepository.save(review); + return ReviewUpdateResponse.from(updateReview.getId()); + } + + @Transactional + public ReviewDeleteResponse deleteReview(final Long userId,final Long reviewId) { + // 삭제된 후기를 삭제할 경우 + Review review = reviewRepository.findByIdAndStatusIn(reviewId, + List.of(ReviewStatus.ACTIVE, ReviewStatus.PRIVATE)) + .orElseThrow(() -> new BeApplicationException(ErrorCodes.REVIEW_DELETE_INVALID, + HttpStatus.NOT_FOUND)); + + // 유저가 작성한 후기가 아닌 경우 + if (!review.getUser().getId().equals(userId)) { + throw new BeApplicationException(ErrorCodes.REVIEW_DELETE_NOT_USER, HttpStatus.FORBIDDEN); + } + + // 후기 삭제 + review.delete(); + reviewRepository.save(review); + return ReviewDeleteResponse.from(true); + } + + @Transactional + public ReviewToggleLikeResponse toggleLikeReview(Long userId, Long reviewId) { + // 삭제된 후기를 좋아요 할 경우 + Review review = reviewRepository.findByIdAndStatusIn(reviewId, + List.of(ReviewStatus.ACTIVE, ReviewStatus.PRIVATE)) + .orElseThrow(() -> new BeApplicationException(ErrorCodes.REVIEW_LIKE_INVALID, + HttpStatus.NOT_FOUND)); + + UserLikeReview userLikeReview = userLikeReviewRepository.findByUserIdAndReviewId(userId, + reviewId) + .orElse(null); + + // 처음 좋아요를 누른 게 아닌 경우 + if (userLikeReview != null) { + userLikeReview.toggleStatus(); + } else { + // 처음 좋아요를 누른 경우 - 새로 생성 + User user = User.of(userId); + userLikeReview = UserLikeReview.of(user, review); + } + + UserLikeReview saveLikeReview = userLikeReviewRepository.save(userLikeReview); + return ReviewToggleLikeResponse.from(saveLikeReview.getStatus()); } } diff --git a/src/main/java/site/travellaboratory/be/service/UserService.java b/src/main/java/site/travellaboratory/be/service/UserService.java index 114f777d..567ba6e7 100644 --- a/src/main/java/site/travellaboratory/be/service/UserService.java +++ b/src/main/java/site/travellaboratory/be/service/UserService.java @@ -1,57 +1,78 @@ package site.travellaboratory.be.service; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; +import java.io.IOException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import site.travellaboratory.be.common.exception.BeApplicationException; import site.travellaboratory.be.common.exception.ErrorCodes; -import site.travellaboratory.be.controller.user.dto.ProfileImgUpdateRequest; -import site.travellaboratory.be.controller.user.dto.ProfileImgUpdateResponse; import site.travellaboratory.be.controller.user.dto.UserProfileResponse; import site.travellaboratory.be.controller.user.dto.UserProfileUpdateRequest; import site.travellaboratory.be.controller.user.dto.UserProfileUpdateResponse; -import site.travellaboratory.be.domain.user.entity.User; import site.travellaboratory.be.domain.user.UserRepository; +import site.travellaboratory.be.domain.user.entity.User; import site.travellaboratory.be.domain.user.entity.UserStatus; @Service @RequiredArgsConstructor +@Transactional public class UserService { + @Value("${cloud.aws.s3.bucket}") + private String bucket; + private final UserRepository userRepository; + private final AmazonS3Client amazonS3Client; + public UserProfileResponse findByUserProfile(final Long userId) { final User user = userRepository.findByIdAndStatus(userId, UserStatus.ACTIVE) - .orElseThrow(() -> new BeApplicationException(ErrorCodes.AUTH_USER_NOT_FOUND, - HttpStatus.BAD_REQUEST) - ); + .orElseThrow(() -> new BeApplicationException(ErrorCodes.AUTH_USER_NOT_FOUND, + HttpStatus.BAD_REQUEST) + ); return UserProfileResponse.from(user); } public UserProfileUpdateResponse updateProfile( + final MultipartFile file, final UserProfileUpdateRequest userProfileUpdateRequest, final Long userId ) { - final User user = userRepository.getById(userId); - final User updatedUser = user.update(userProfileUpdateRequest.nickname(), userProfileUpdateRequest.introduce()); + final User user = userRepository.findByIdAndStatus(userId, UserStatus.ACTIVE) + .orElseThrow(() -> new BeApplicationException(ErrorCodes.USER_NOT_FOUND, + HttpStatus.NOT_FOUND)); + + final String url = uploadFiles(file); + + final User updatedUser = user.update(userProfileUpdateRequest.username(), userProfileUpdateRequest.nickname(), + url, userProfileUpdateRequest.introduce()); userRepository.save(updatedUser); - final UserProfileUpdateResponse userProfileUpdateResponse = new UserProfileUpdateResponse( - userProfileUpdateRequest.nickname(), userProfileUpdateRequest.introduce()); - return userProfileUpdateResponse; - } // 프론트에서 유저네임 같은 경우에는 못 바꾸게 그대로 값 넘어오게 설정해야함 그때 맞춰서 파라미터 바꾸기. + return new UserProfileUpdateResponse(updatedUser.getUsername(), updatedUser.getNickname(), + updatedUser.getProfileImgUrl(), + updatedUser.getIntroduce()); + } - public ProfileImgUpdateResponse updateProfileImage( - final ProfileImgUpdateRequest profileImgUpdateRequest, - final Long userId - ) { - final User user = userRepository.getById(userId); - final User updatedProfileImg = user.updateProfileImg(profileImgUpdateRequest.profileImgUrl()); + private String uploadFiles(final MultipartFile file) { + try { + String fileName = file.getOriginalFilename(); + String fileUrl = "https://" + bucket + ".s3.ap-northeast-2.amazonaws.com/" + fileName; - userRepository.save(updatedProfileImg); + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(file.getContentType()); + metadata.setContentLength(file.getSize()); - return new ProfileImgUpdateResponse( - profileImgUpdateRequest.profileImgUrl()); + amazonS3Client.putObject(bucket, fileName, file.getInputStream(), metadata); + return fileUrl; + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("File upload failed", e); + } } } diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 3fb43a20..3990ceaa 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -12,4 +12,17 @@ jwt: plus-hour: ${jwt-refresh-token-plus-hour} servers: - url: ${servers-url} \ No newline at end of file + url: ${servers-url} + +## s3 이미지 저장 +cloud: + aws: + s3: + bucket: ${s3-bucket} + + credentials: + access-key: ${s3-access-key} + secret-key: ${s3-secret-key} + + region: + static: ${s3-region-static} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f436efef..d9387682 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,6 +20,19 @@ jwt: refresh-token: plus-hour: ${jwt-refresh-token-plus-hour} +## s3 이미지 저장 +cloud: + aws: + s3: + bucket: ${s3-bucket} + + credentials: + access-key: ${s3-access-key} + secret-key: ${s3-secret-key} + + region: + static: ${s3-region-static} + --- spring: config: @@ -27,7 +40,7 @@ spring: on-profile: dev jpa: hibernate: - ddl-auto: create + ddl-auto: update show-sql: true properties: hibernate: @@ -48,4 +61,4 @@ spring: ddl-auto: update logging: level: - root: INFO \ No newline at end of file + root: INFO