diff --git a/build.gradle b/build.gradle index d1b4241..4d814bb 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,17 @@ dependencies { annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + //smtp + implementation 'org.springframework.boot:spring-boot-starter-mail' + + // s3 + implementation(enforcedPlatform("com.amazonaws:aws-java-sdk-bom:1.11.1000")) + implementation("com.amazonaws:aws-java-sdk-s3") + implementation("javax.xml.bind:jaxb-api:2.3.1") + } tasks.named('test') { diff --git a/src/main/java/org/jullaene/walkmong_back/api/apply/repository/ApplyRepository.java b/src/main/java/org/jullaene/walkmong_back/api/apply/repository/ApplyRepository.java index 56c9595..2c300e4 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/apply/repository/ApplyRepository.java +++ b/src/main/java/org/jullaene/walkmong_back/api/apply/repository/ApplyRepository.java @@ -1,9 +1,12 @@ package org.jullaene.walkmong_back.api.apply.repository; import org.jullaene.walkmong_back.api.apply.domain.Apply; +import org.jullaene.walkmong_back.api.apply.domain.enums.MatchingStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface ApplyRepository extends JpaRepository { + boolean existsByBoardIdAndMemberIdAndDelYn(Long boardId, Long memberId, String delYn); + boolean existsByBoardIdAndMemberIdAndMatchingStatusAndDelYn(Long boardId, Long memberId, MatchingStatus matchingStatus, String delYn); } diff --git a/src/main/java/org/jullaene/walkmong_back/api/apply/service/ApplyService.java b/src/main/java/org/jullaene/walkmong_back/api/apply/service/ApplyService.java index 8dbc122..2c7da2b 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/apply/service/ApplyService.java +++ b/src/main/java/org/jullaene/walkmong_back/api/apply/service/ApplyService.java @@ -1,11 +1,22 @@ package org.jullaene.walkmong_back.api.apply.service; import lombok.RequiredArgsConstructor; +import org.jullaene.walkmong_back.api.apply.domain.enums.MatchingStatus; import org.jullaene.walkmong_back.api.apply.repository.ApplyRepository; +import org.jullaene.walkmong_back.common.exception.CustomException; +import org.jullaene.walkmong_back.common.exception.ErrorType; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class ApplyService { private final ApplyRepository applyRepository; + @Transactional(readOnly = true) + public void isValidWalkerByBoardIdAndMatchingStatus(Long memberId, Long boardId, MatchingStatus matchingStatus) { + if (!applyRepository.existsByBoardIdAndMemberIdAndMatchingStatusAndDelYn(boardId, memberId, matchingStatus, "N")) { + throw new CustomException(HttpStatus.UNAUTHORIZED, ErrorType.ACCESS_DENIED); + } + } } diff --git a/src/main/java/org/jullaene/walkmong_back/api/board/domain/Review.java b/src/main/java/org/jullaene/walkmong_back/api/board/domain/Review.java deleted file mode 100644 index 7b29a9b..0000000 --- a/src/main/java/org/jullaene/walkmong_back/api/board/domain/Review.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.jullaene.walkmong_back.api.board.domain; - -import jakarta.persistence.*; -import org.hibernate.annotations.Comment; -import org.hibernate.annotations.DynamicUpdate; -import org.jullaene.walkmong_back.common.BaseEntity; - -@Table(name = "review") -@Entity -@DynamicUpdate -public class Review extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "review_id") - private Long reviewId; - - @Comment("게시글 아이디") - private Long postId; - - @Comment("후기 작성자 아이디") - private Long reviewerId; - - @Comment("후기 대상자 아이디") - private Long reviewTargetId; - - @Comment("내용") - private String content; - - @Comment("평점") - private Integer rating; -} diff --git a/src/main/java/org/jullaene/walkmong_back/api/board/repository/BoardRepository.java b/src/main/java/org/jullaene/walkmong_back/api/board/repository/BoardRepository.java index b27a723..c8bb9dd 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/board/repository/BoardRepository.java +++ b/src/main/java/org/jullaene/walkmong_back/api/board/repository/BoardRepository.java @@ -6,4 +6,5 @@ @Repository public interface BoardRepository extends JpaRepository { + boolean existsByOwnerIdAndBoardIdAndDelYn(Long ownerId, Long boardId, String delYn); } diff --git a/src/main/java/org/jullaene/walkmong_back/api/board/repository/ReviewRepository.java b/src/main/java/org/jullaene/walkmong_back/api/board/repository/ReviewRepository.java deleted file mode 100644 index 46599f2..0000000 --- a/src/main/java/org/jullaene/walkmong_back/api/board/repository/ReviewRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.jullaene.walkmong_back.api.board.repository; - -import org.jullaene.walkmong_back.api.board.domain.Review; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface ReviewRepository extends JpaRepository { -} diff --git a/src/main/java/org/jullaene/walkmong_back/api/board/rest/ReviewController.java b/src/main/java/org/jullaene/walkmong_back/api/board/rest/ReviewController.java deleted file mode 100644 index 13e0f6c..0000000 --- a/src/main/java/org/jullaene/walkmong_back/api/board/rest/ReviewController.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.jullaene.walkmong_back.api.board.rest; - -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.jullaene.walkmong_back.api.board.service.ReviewService; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Review", description = "리뷰 정보 관련 api 입니다.") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/review") -public class ReviewController { - private final ReviewService reviewService; -} diff --git a/src/main/java/org/jullaene/walkmong_back/api/board/service/BoardService.java b/src/main/java/org/jullaene/walkmong_back/api/board/service/BoardService.java index 689c391..389795a 100644 --- a/src/main/java/org/jullaene/walkmong_back/api/board/service/BoardService.java +++ b/src/main/java/org/jullaene/walkmong_back/api/board/service/BoardService.java @@ -1,11 +1,30 @@ package org.jullaene.walkmong_back.api.board.service; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.jullaene.walkmong_back.api.board.repository.BoardRepository; +import org.jullaene.walkmong_back.common.exception.CustomException; +import org.jullaene.walkmong_back.common.exception.ErrorType; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j @Service @RequiredArgsConstructor public class BoardService { private final BoardRepository boardRepository; + /** + * 게시글 아이디와 삭제 여부로 해당 게시글의 유효한 반려인인지 확인 + * */ + @Transactional(readOnly = true) + public void isValidOwnerByBoardIdAndDelYn (Long memberId, Long boardId, String delYn) { + log.info("memberId : " + memberId + " boardId : " + boardId); + if (!boardRepository.existsByOwnerIdAndBoardIdAndDelYn(memberId, boardId, delYn)) { + throw new CustomException(HttpStatus.UNAUTHORIZED, ErrorType.ACCESS_DENIED); + } + } + + } diff --git a/src/main/java/org/jullaene/walkmong_back/api/board/service/ReviewService.java b/src/main/java/org/jullaene/walkmong_back/api/board/service/ReviewService.java deleted file mode 100644 index 3a75452..0000000 --- a/src/main/java/org/jullaene/walkmong_back/api/board/service/ReviewService.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.jullaene.walkmong_back.api.board.service; - -import lombok.RequiredArgsConstructor; -import org.jullaene.walkmong_back.api.board.repository.ReviewRepository; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ReviewService { - private final ReviewRepository reviewRepository; -} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/domain/HashtagToWalker.java b/src/main/java/org/jullaene/walkmong_back/api/review/domain/HashtagToWalker.java new file mode 100644 index 0000000..43227c0 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/domain/HashtagToWalker.java @@ -0,0 +1,37 @@ +package org.jullaene.walkmong_back.api.review.domain; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; +import org.jullaene.walkmong_back.api.review.domain.enums.HashtagWalkerNm; +import org.jullaene.walkmong_back.common.BaseEntity; + +@Table(name = "hashtag_to_walker") +@Entity +@NoArgsConstructor +@DynamicUpdate +public class HashtagToWalker extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "hashtag_to_walker_id", nullable = false) + private Long hashtagToWalkerId; + + @Comment("후기 아이디") + private Long reviewToWalkerId; + + @Comment("후기 대상 아이디") + private Long reviewTargetId; + + @Comment("해시태그 명") + @Enumerated(EnumType.STRING) + private HashtagWalkerNm hashtagWalkerNm; + + @Builder + public HashtagToWalker(Long reviewToWalkerId, Long reviewTargetId, HashtagWalkerNm hashtagWalkerNm) { + this.reviewToWalkerId = reviewToWalkerId; + this.reviewTargetId = reviewTargetId; + this.hashtagWalkerNm = hashtagWalkerNm; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToOwner.java b/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToOwner.java new file mode 100644 index 0000000..5f98fe4 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToOwner.java @@ -0,0 +1,71 @@ +package org.jullaene.walkmong_back.api.review.domain; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; +import org.jullaene.walkmong_back.api.review.domain.enums.Activity; +import org.jullaene.walkmong_back.api.review.domain.enums.Aggressiveness; +import org.jullaene.walkmong_back.api.review.domain.enums.Sociality; +import org.jullaene.walkmong_back.api.review.dto.req.ReviewToOwnerReqDto; +import org.jullaene.walkmong_back.common.BaseEntity; + +@Table(name = "review_to_owner") +@Entity +@NoArgsConstructor +@DynamicUpdate +public class ReviewToOwner extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "review_to_owner_id", nullable = false) + private Long reviewToOwnerId; + + @Comment("게시글 아이디") + private Long boardId; + + @Comment("후기 작성자 아이디") + private Long reviewerId; + + @Comment("후기 대상자 아이디") + private Long reviewTargetId; + + @Comment("후기") + private String content; + + @Comment("좋음/싫음") + @Column(name = "good_yn", columnDefinition = "VARCHAR(1) default 'N'") + private String goodYn; + + @Comment("사회성") + @Enumerated(EnumType.STRING) + private Sociality sociality; + + @Comment("활동량") + @Enumerated(EnumType.STRING) + private Activity activity; + + @Comment("공격력") + @Enumerated(EnumType.STRING) + private Aggressiveness aggressiveness; + + @Comment("아쉬운 점") + private String disappointment; + + @Builder + public ReviewToOwner(ReviewToOwnerReqDto reviewToOwnerReqDto, Long reviewerId) { + this.boardId = reviewToOwnerReqDto.getBoardId(); + this.reviewerId = reviewerId; + this.reviewTargetId = reviewToOwnerReqDto.getOwnerId(); + this.goodYn = reviewToOwnerReqDto.getGoodYn(); + this.content = reviewToOwnerReqDto.getContent(); + this.sociality = reviewToOwnerReqDto.getSociality(); + this.activity = reviewToOwnerReqDto.getActivity(); + this.aggressiveness = reviewToOwnerReqDto.getAggressiveness(); + this.disappointment = reviewToOwnerReqDto.getDisappointment(); + } + + public Long getReviewToOwnerId () { + return reviewToOwnerId; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToOwnerImage.java b/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToOwnerImage.java new file mode 100644 index 0000000..f70b044 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToOwnerImage.java @@ -0,0 +1,30 @@ +package org.jullaene.walkmong_back.api.review.domain; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; + +@Table(name = "review_to_owner_image") +@Entity +@NoArgsConstructor +@DynamicUpdate +public class ReviewToOwnerImage { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "review_to_owner_image_id", nullable = false) + private Long reviewToOwnerImageId; + + @Comment("반려인 후기 아이디") + private Long reviewToOwnerId; + + @Comment("이미지") + private String imageUrl; + + @Builder + public ReviewToOwnerImage(Long reviewToOwnerId, String imageUrl) { + this.reviewToOwnerId = reviewToOwnerId; + this.imageUrl = imageUrl; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToWalker.java b/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToWalker.java new file mode 100644 index 0000000..6510ed5 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToWalker.java @@ -0,0 +1,64 @@ +package org.jullaene.walkmong_back.api.review.domain; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; +import org.jullaene.walkmong_back.api.review.dto.req.ReviewToWalkerReqDto; +import org.jullaene.walkmong_back.common.BaseEntity; + +@Table(name = "review_to_walker") +@Entity +@NoArgsConstructor +@DynamicUpdate +public class ReviewToWalker extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "review_to_walker_id", nullable = false) + private Long reviewToWalkerId; + + @Comment("게시글 아이디") + private Long boardId; + + @Comment("후기 작성자 아이디") + private Long reviewerId; + + @Comment("후기 대상자 아이디") + private Long reviewTargetId; + + @Comment("후기") + private String content; + + @Comment("시간 약속 준수 여부") + private Float timePunctuality; + + @Comment("소통") + private Float communication; + + @Comment("태도") + private Float attitude; + + @Comment("산책 요청사항 이행") + private Float taskCompletion; + + @Comment("산책 중 사진 공유") + private Float photoSharing; + + @Builder + public ReviewToWalker(ReviewToWalkerReqDto reviewToWalkerReqDto, Long reviewerId) { + this.boardId = reviewToWalkerReqDto.getBoardId(); + this.reviewerId = reviewerId; + this.reviewTargetId = reviewToWalkerReqDto.getWalkerId(); + this.content = reviewToWalkerReqDto.getContent(); + this.timePunctuality = reviewToWalkerReqDto.getTimePunctuality(); + this.communication = reviewToWalkerReqDto.getCommunication(); + this.attitude = reviewToWalkerReqDto.getAttitude(); + this.taskCompletion = reviewToWalkerReqDto.getTaskCompletion(); + this.photoSharing = reviewToWalkerReqDto.getPhotoSharing(); + } + + public Long getReviewToWalkerId () { + return reviewToWalkerId; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToWalkerImage.java b/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToWalkerImage.java new file mode 100644 index 0000000..748f45d --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/domain/ReviewToWalkerImage.java @@ -0,0 +1,30 @@ +package org.jullaene.walkmong_back.api.review.domain; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; + +@Table(name = "review_to_walker_image") +@Entity +@NoArgsConstructor +@DynamicUpdate +public class ReviewToWalkerImage { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "review_to_walker_image_id", nullable = false) + private Long reviewToWalkerImageId; + + @Comment("산책자 후기 아이디") + private Long reviewToWalkerId; + + @Comment("이미지") + private String imageUrl; + + @Builder + public ReviewToWalkerImage(Long reviewToWalkerId, String imageUrl) { + this.reviewToWalkerId = reviewToWalkerId; + this.imageUrl = imageUrl; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/Activity.java b/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/Activity.java new file mode 100644 index 0000000..ecdbad7 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/Activity.java @@ -0,0 +1,15 @@ +package org.jullaene.walkmong_back.api.review.domain.enums; + +public enum Activity { + RUNNING_CONSTANTLY("계속 뜀"), + RUNNING_OCCASIONALLY("가끔 뜀"), + WALKING_FAST("빠르게 걸음"), + WALKING_SLOWLY("천천히 걸음"), + FREQUENTLY_STOPPING("자주 멈춤"); + + private final String name; + + Activity(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/Aggressiveness.java b/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/Aggressiveness.java new file mode 100644 index 0000000..57e5d6a --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/Aggressiveness.java @@ -0,0 +1,15 @@ +package org.jullaene.walkmong_back.api.review.domain.enums; + +public enum Aggressiveness { + DOCILE("온순"), + OCCASIONAL_BARKING("가끔 짖음"), + FREQUENT_BARKING("자주 짖음"), + BITING("물음"), + NIPPING("입질"); + + private final String name; + + Aggressiveness(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/DogOwnership.java b/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/DogOwnership.java new file mode 100644 index 0000000..cc48e6a --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/DogOwnership.java @@ -0,0 +1,15 @@ +package org.jullaene.walkmong_back.api.review.domain.enums; + +public enum DogOwnership { + NONE("없음"), + LESS_THAN_3("3년 미만"), + MORE_THAN_3("3년 이상"), + MORE_THAN_5("5년 이상"), + MORE_THAN_10("10년 이상"); + + private final String name; + + DogOwnership(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/HashtagWalkerNm.java b/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/HashtagWalkerNm.java new file mode 100644 index 0000000..ba503c8 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/HashtagWalkerNm.java @@ -0,0 +1,21 @@ +package org.jullaene.walkmong_back.api.review.domain.enums; + +public enum HashtagWalkerNm { + LIKED_BY_DOG("반려견이 좋아해요"), + POLITE("매너가 좋아요"), + DETAIL_ORIENTED("꼼꼼해요"), + GOOD_SCHEDULE_MANAGEMENT("일정 조정을 잘 해줘요"), + RESPONSIBLE_WALKING("산책을 성실히 해줘요"), + GOOD_WITH_DOGS("반려견을 잘 다뤄요"), + FAST_RESPONSE("답장이 빨라요"), + FOLLOWS_REQUESTS("요청 사항을 잘 들어줘요"), + RELIABLE("믿고 맡길 수 있어요"), + SAFE_WALKING("안전한 산책을 제공해요"), + PROFESSIONAL("전문적으로 느껴져요"); + + private final String name; + + HashtagWalkerNm(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/Sociality.java b/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/Sociality.java new file mode 100644 index 0000000..8e0e29a --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/domain/enums/Sociality.java @@ -0,0 +1,15 @@ +package org.jullaene.walkmong_back.api.review.domain.enums; + +public enum Sociality { + PEOPLE_FRIENDLY("사람 좋아함"), + DOG_FRIENDLY("강아지 좋아함"), + SHY("낯가림 있음"), + PLAYFUL("애교 많음"), + GUARDED("경계심 심함"); + + private final String name; + + Sociality(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/dto/req/ReviewToOwnerReqDto.java b/src/main/java/org/jullaene/walkmong_back/api/review/dto/req/ReviewToOwnerReqDto.java new file mode 100644 index 0000000..878a64f --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/dto/req/ReviewToOwnerReqDto.java @@ -0,0 +1,36 @@ +package org.jullaene.walkmong_back.api.review.dto.req; + +import lombok.Builder; +import lombok.Getter; +import org.jullaene.walkmong_back.api.review.domain.enums.Activity; +import org.jullaene.walkmong_back.api.review.domain.enums.Aggressiveness; +import org.jullaene.walkmong_back.api.review.domain.enums.Sociality; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Getter +public class ReviewToOwnerReqDto { + private final Long ownerId; + private final Long boardId; + private final String goodYn; + private final Sociality sociality; + private final Activity activity; + private final Aggressiveness aggressiveness; + private final String disappointment; + private final String content; + private final List images; + + @Builder + public ReviewToOwnerReqDto(Long ownerId, Long boardId, String goodYn, Sociality sociality, Activity activity, Aggressiveness aggressiveness, String disappointment, String content, List images) { + this.ownerId = ownerId; + this.boardId = boardId; + this.goodYn = goodYn; + this.sociality = sociality; + this.activity = activity; + this.aggressiveness = aggressiveness; + this.disappointment = disappointment; + this.content = content; + this.images = images; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/dto/req/ReviewToWalkerReqDto.java b/src/main/java/org/jullaene/walkmong_back/api/review/dto/req/ReviewToWalkerReqDto.java new file mode 100644 index 0000000..21f83db --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/dto/req/ReviewToWalkerReqDto.java @@ -0,0 +1,34 @@ +package org.jullaene.walkmong_back.api.review.dto.req; + +import lombok.Getter; +import org.jullaene.walkmong_back.api.review.domain.enums.HashtagWalkerNm; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Getter +public class ReviewToWalkerReqDto { + private final Long walkerId; + private final Long boardId; + private final Float timePunctuality; + private final Float communication; + private final Float attitude; + private final Float taskCompletion; + private final Float photoSharing; + private final String content; + private final List hashtags; + private final List images; + + public ReviewToWalkerReqDto(Long walkerId, Long boardId, Float timePunctuality, Float communication, Float attitude, Float taskCompletion, Float photoSharing, String content, List hashtags, List images) { + this.walkerId = walkerId; + this.boardId = boardId; + this.timePunctuality = timePunctuality; + this.communication = communication; + this.attitude = attitude; + this.taskCompletion = taskCompletion; + this.photoSharing = photoSharing; + this.content = content; + this.hashtags = hashtags; + this.images = images; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/repository/HashtagToWalkerRepository.java b/src/main/java/org/jullaene/walkmong_back/api/review/repository/HashtagToWalkerRepository.java new file mode 100644 index 0000000..1acc064 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/repository/HashtagToWalkerRepository.java @@ -0,0 +1,9 @@ +package org.jullaene.walkmong_back.api.review.repository; + +import org.jullaene.walkmong_back.api.review.domain.HashtagToWalker; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface HashtagToWalkerRepository extends JpaRepository { +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToOwnerImageRepository.java b/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToOwnerImageRepository.java new file mode 100644 index 0000000..3154c4c --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToOwnerImageRepository.java @@ -0,0 +1,7 @@ +package org.jullaene.walkmong_back.api.review.repository; + +import org.jullaene.walkmong_back.api.review.domain.ReviewToOwnerImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewToOwnerImageRepository extends JpaRepository { +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToOwnerRepository.java b/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToOwnerRepository.java new file mode 100644 index 0000000..09e1579 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToOwnerRepository.java @@ -0,0 +1,7 @@ +package org.jullaene.walkmong_back.api.review.repository; + +import org.jullaene.walkmong_back.api.review.domain.ReviewToOwner; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewToOwnerRepository extends JpaRepository { +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToWalkerImageRepository.java b/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToWalkerImageRepository.java new file mode 100644 index 0000000..5f6b4a6 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToWalkerImageRepository.java @@ -0,0 +1,7 @@ +package org.jullaene.walkmong_back.api.review.repository; + +import org.jullaene.walkmong_back.api.review.domain.ReviewToWalkerImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewToWalkerImageRepository extends JpaRepository { +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToWalkerRepository.java b/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToWalkerRepository.java new file mode 100644 index 0000000..c06cc72 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/repository/ReviewToWalkerRepository.java @@ -0,0 +1,7 @@ +package org.jullaene.walkmong_back.api.review.repository; + +import org.jullaene.walkmong_back.api.review.domain.ReviewToWalker; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewToWalkerRepository extends JpaRepository { +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/rest/ReviewToOwnerController.java b/src/main/java/org/jullaene/walkmong_back/api/review/rest/ReviewToOwnerController.java new file mode 100644 index 0000000..be7d113 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/rest/ReviewToOwnerController.java @@ -0,0 +1,22 @@ +package org.jullaene.walkmong_back.api.review.rest; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.jullaene.walkmong_back.api.review.dto.req.ReviewToOwnerReqDto; +import org.jullaene.walkmong_back.api.review.service.ReviewToOwnerService; +import org.jullaene.walkmong_back.common.BasicResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/review/to/owner") +public class ReviewToOwnerController { + private final ReviewToOwnerService reviewToOwnerService; + + + @PostMapping("/register") + public ResponseEntity> registerReviewToOwner(@Valid @ModelAttribute ReviewToOwnerReqDto reviewToOwnerReqDto) { + return ResponseEntity.ok(BasicResponse.ofSuccess(reviewToOwnerService.registerReviewToOwner(reviewToOwnerReqDto))); + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/rest/ReviewToWalkerController.java b/src/main/java/org/jullaene/walkmong_back/api/review/rest/ReviewToWalkerController.java new file mode 100644 index 0000000..73d88f9 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/rest/ReviewToWalkerController.java @@ -0,0 +1,26 @@ +package org.jullaene.walkmong_back.api.review.rest; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.jullaene.walkmong_back.api.review.dto.req.ReviewToWalkerReqDto; +import org.jullaene.walkmong_back.api.review.service.ReviewToWalkerService; +import org.jullaene.walkmong_back.common.BasicResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/review/to/walker") +public class ReviewToWalkerController { + + private final ReviewToWalkerService reviewToWalkerService; + + + @PostMapping("/register") + public ResponseEntity> registerReviewToOwner(@Valid @ModelAttribute ReviewToWalkerReqDto reviewToWalkerReqDto) { + return ResponseEntity.ok(BasicResponse.ofSuccess(reviewToWalkerService.registerReviewToWalker(reviewToWalkerReqDto))); + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/service/ReviewToOwnerService.java b/src/main/java/org/jullaene/walkmong_back/api/review/service/ReviewToOwnerService.java new file mode 100644 index 0000000..430754d --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/service/ReviewToOwnerService.java @@ -0,0 +1,56 @@ +package org.jullaene.walkmong_back.api.review.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jullaene.walkmong_back.api.apply.domain.enums.MatchingStatus; +import org.jullaene.walkmong_back.api.apply.service.ApplyService; +import org.jullaene.walkmong_back.api.member.domain.Member; +import org.jullaene.walkmong_back.api.member.service.MemberService; +import org.jullaene.walkmong_back.api.review.domain.ReviewToOwner; +import org.jullaene.walkmong_back.api.review.domain.ReviewToOwnerImage; +import org.jullaene.walkmong_back.api.review.dto.req.ReviewToOwnerReqDto; +import org.jullaene.walkmong_back.api.review.repository.ReviewToOwnerImageRepository; +import org.jullaene.walkmong_back.api.review.repository.ReviewToOwnerRepository; +import org.jullaene.walkmong_back.common.file.FileService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ReviewToOwnerService { + private final MemberService memberService; + private final ApplyService applyService; + private final FileService fileService; + private final ReviewToOwnerRepository reviewToOwnerRepository; + private final ReviewToOwnerImageRepository reviewToOwnerImageRepository; + public Long registerReviewToOwner(ReviewToOwnerReqDto reviewToOwnerReqDto) { + Member member = memberService.getMemberFromUserDetail(); + + applyService.isValidWalkerByBoardIdAndMatchingStatus(member.getMemberId(), reviewToOwnerReqDto.getBoardId(), MatchingStatus.CONFIRMED); + log.info("해당 산책의 산책자 인증 완료"); + + ReviewToOwner reviewToOwner = ReviewToOwner.builder() + .reviewToOwnerReqDto(reviewToOwnerReqDto) + .reviewerId(member.getMemberId()) + .build(); + + Long reviewToOwnerId = reviewToOwnerRepository.save(reviewToOwner).getReviewToOwnerId(); + log.info("산책자 -> 반려인 리뷰 작성 완료 : " + reviewToOwnerId); + + List images = reviewToOwnerReqDto.getImages().stream() + .map(image -> { + String imageUrl = fileService.uploadFile(image, "/review/to/walker"); + return ReviewToOwnerImage.builder() + .reviewToOwnerId(reviewToOwnerId) + .imageUrl(imageUrl) + .build(); + }) + .toList(); + reviewToOwnerImageRepository.saveAll(images); + log.info("산책자 -> 반려인 리뷰 이미지 저장 완료"); + + return reviewToOwnerId; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/api/review/service/ReviewToWalkerService.java b/src/main/java/org/jullaene/walkmong_back/api/review/service/ReviewToWalkerService.java new file mode 100644 index 0000000..356c8da --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/api/review/service/ReviewToWalkerService.java @@ -0,0 +1,72 @@ +package org.jullaene.walkmong_back.api.review.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jullaene.walkmong_back.api.board.service.BoardService; +import org.jullaene.walkmong_back.api.member.domain.Member; +import org.jullaene.walkmong_back.api.member.service.MemberService; +import org.jullaene.walkmong_back.api.review.domain.HashtagToWalker; +import org.jullaene.walkmong_back.api.review.domain.ReviewToWalker; +import org.jullaene.walkmong_back.api.review.domain.ReviewToWalkerImage; +import org.jullaene.walkmong_back.api.review.dto.req.ReviewToWalkerReqDto; +import org.jullaene.walkmong_back.api.review.repository.HashtagToWalkerRepository; +import org.jullaene.walkmong_back.api.review.repository.ReviewToWalkerImageRepository; +import org.jullaene.walkmong_back.api.review.repository.ReviewToWalkerRepository; +import org.jullaene.walkmong_back.common.file.FileService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ReviewToWalkerService { + private final MemberService memberService; + private final BoardService boardService; + private final FileService fileService; + private final ReviewToWalkerRepository reviewToWalkerRepository; + private final HashtagToWalkerRepository hashtagToWalkerRepository; + private final ReviewToWalkerImageRepository reviewToWalkerImageRepository; + + @Transactional + public Long registerReviewToWalker(ReviewToWalkerReqDto reviewToWalkerReqDto) { + Member member = memberService.getMemberFromUserDetail(); + + boardService.isValidOwnerByBoardIdAndDelYn(member.getMemberId(), reviewToWalkerReqDto.getBoardId(), "N"); + log.info("해당 게시글의 반려인 인증 완료"); + + ReviewToWalker reviewToWalker = ReviewToWalker.builder() + .reviewToWalkerReqDto(reviewToWalkerReqDto) + .reviewerId(member.getMemberId()) + .build(); + Long reviewToWalkerId = reviewToWalkerRepository.save(reviewToWalker).getReviewToWalkerId(); + log.info("산책자 리뷰 저장"); + + List hashtagToWalkers = reviewToWalkerReqDto.getHashtags().stream() + .map(hashtag -> { + return HashtagToWalker.builder() + .reviewToWalkerId(reviewToWalkerId) + .reviewTargetId(member.getMemberId()) + .hashtagWalkerNm(hashtag) + .build(); + }) + .toList(); + hashtagToWalkerRepository.saveAll(hashtagToWalkers); + log.info("산책자 리뷰 해시태그 저장 완료"); + + List images = reviewToWalkerReqDto.getImages().stream() + .map(image -> { + String imageUrl = fileService.uploadFile(image, "/review/to/walker"); + return ReviewToWalkerImage.builder() + .reviewToWalkerId(reviewToWalkerId) + .imageUrl(imageUrl) + .build(); + }) + .toList(); + reviewToWalkerImageRepository.saveAll(images); + log.info("산책자 리뷰 이미지 저장 완료"); + + return reviewToWalkerId; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/common/exception/ErrorType.java b/src/main/java/org/jullaene/walkmong_back/common/exception/ErrorType.java index 3a81491..bbe06c5 100644 --- a/src/main/java/org/jullaene/walkmong_back/common/exception/ErrorType.java +++ b/src/main/java/org/jullaene/walkmong_back/common/exception/ErrorType.java @@ -12,12 +12,21 @@ public enum ErrorType { LOGIN_REQUIRED("로그인 해주세요."), EXPIRED_TOKEN("만료된 요청입니다."), ALREADY_EXIST_USER("이미 존재하는 유저입니다."), + ALREADY_EXIST_NICKNAME("이미 존재하는 닉네임입니다."), + INVALID_VERIFICATION_CODE("인증에 실패했습니다."), ALREADY_LOGIN("로그인 상태입니다"), WRONG_PASSWORD("잘못된 비밀번호 입니다."), REQUEST_VALIDATION_ERROR("유효성 검사가 실패하였습니다."), INVALID_TOKEN("유효하지 않은 토큰입니다."), INTERNAL_SERVER("서버 오류입니다."), UNAUTHORIZED_UPDATE("수정 권한이 없는 유저입니다."), + DOG_NOT_FOUND("존재하지 않는 강아지입니다."), + INVALID_ADDRESS("유효하지 않은 주소입니다."), + CANNOT_SELF_APPLY("본인의 게시글에는 지원할 수 없습니다."), + CANNOT_DUPLICATED_APPLY("한 게시글에 여러 번 지원할 수 없습니다."), + CANNOT_DUPLICATED_DOG_PROFILE("이미 등록된 강아지입니다"), + INVALID_FILE("존재하지 않는 파일입니다."), + INVALID_S3_FILE("AWS S3 저장소에 존재하지 않는 파일입니다."), ; private String message; diff --git a/src/main/java/org/jullaene/walkmong_back/common/file/FileService.java b/src/main/java/org/jullaene/walkmong_back/common/file/FileService.java new file mode 100644 index 0000000..137cc5c --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/common/file/FileService.java @@ -0,0 +1,107 @@ +package org.jullaene.walkmong_back.common.file; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.*; +import com.amazonaws.util.IOUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jullaene.walkmong_back.common.exception.CustomException; +import org.jullaene.walkmong_back.common.exception.ErrorType; +import org.jullaene.walkmong_back.common.utils.FileUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +public class FileService { + + @Value("${cloud.aws.bucket}") + private String bucket; + private final AmazonS3Client s3Client; + + /** + * S3 file upload + * @param multipartFile file + * @return file url + */ + public String uploadFile(MultipartFile multipartFile, String dirPath) { + FileUtils.checkInvalidUploadFile(multipartFile); + + try { + // Set metadata for the file + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(multipartFile.getContentType()); + objectMetadata.setContentLength(multipartFile.getSize()); + + // Construct file path + String filePath = dirPath + "/" + multipartFile.getOriginalFilename(); + + // Upload file to S3 + s3Client.putObject(new PutObjectRequest(bucket, filePath, multipartFile.getInputStream(), objectMetadata)); + + // Generate file URL + String uploadFileUrl = s3Client.getUrl(bucket, filePath).toString(); + log.info("File upload success : {}", uploadFileUrl); + + return uploadFileUrl; + } catch (IOException e) { + log.error("File upload failed : {}", e.getMessage(), e); + throw new RuntimeException("File upload failed", e); + } + } + + /** + * S3 file delete + * @return Failed Delete Object Counts + */ + public int deleteFile (Integer idCount, List keys) { + DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(bucket).withKeys(keys); + DeleteObjectsResult result = s3Client.deleteObjects(deleteObjectsRequest); + int count = idCount - result.getDeletedObjects().size(); + log.info("Failed Delete Object Counts = {}", idCount - result.getDeletedObjects().size()); + return count; + } + + /** + * S3 file download + */ + public ResponseEntity downloadFile (String originFileName, String filePath) { + byte[] bytes = null; + HttpHeaders httpHeaders = new HttpHeaders(); + + try { + S3Object o = s3Client.getObject(new GetObjectRequest(bucket, filePath)); + if (o == null) throw new CustomException(HttpStatus.NOT_FOUND, ErrorType.INVALID_S3_FILE); + + S3ObjectInputStream objectInputStream = o.getObjectContent(); + + bytes = IOUtils.toByteArray(objectInputStream); + + String fileName = URLEncoder.encode(originFileName, String.valueOf(StandardCharsets.UTF_8)).replaceAll("\\+", "%20"); + httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM); + httpHeaders.setContentLength(bytes.length); + httpHeaders.setContentDispositionFormData("attachment", fileName); + + } catch (AmazonS3Exception e) { + log.error("File not find : {}", e.getMessage()); + throw new CustomException(HttpStatus.NOT_FOUND, ErrorType.INVALID_S3_FILE); + } catch (IOException e) { + e.printStackTrace(); + log.error("File upload failed : {}", e.getMessage()); + } + + return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK); + } + +} diff --git a/src/main/java/org/jullaene/walkmong_back/common/utils/FileUtils.java b/src/main/java/org/jullaene/walkmong_back/common/utils/FileUtils.java new file mode 100644 index 0000000..9099ffc --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/common/utils/FileUtils.java @@ -0,0 +1,77 @@ +package org.jullaene.walkmong_back.common.utils; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jullaene.walkmong_back.common.exception.CustomException; +import org.jullaene.walkmong_back.common.exception.ErrorType; +import org.springframework.http.HttpStatus; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.UUID; + +@Slf4j +@RequiredArgsConstructor +public class FileUtils { + private static final String BASE_DIR = "freeSign"; + + /** + * UUID 파일명 반환 + */ + public static String getUuidFileName() { + return UUID.randomUUID().toString(); + } + + + /** + * Multipart 의 contentType 값에서 / 이후 확장자만 자름 + * @param contentType ex) image/png + * @return ex) png + */ + public static String getFileType (String contentType) { + if (StringUtils.hasText(contentType)) { + return contentType.substring(contentType.lastIndexOf('/') + 1); + } + return null; + } + + /** + * 파일 전체 경로 생성 + * @param fileId 생성된 파일 고유 ID + * @param fileType 확장자 + */ + public static String createPath (String fileId, String fileType, String folderName, String dirPath) { + return String.format("%s/%s/%s.%s", BASE_DIR + dirPath, folderName, fileId, fileType); + } + + /** + * 년/월/일 폴더명 반환 + */ + public static String getFolderName() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + Date date = new Date(); + String str = sdf.format(date); + return str.replace("-", "/"); + } + + /** + * 파일 유효성 체크 + * @param multipartFile file + */ + public static void checkInvalidUploadFile(MultipartFile multipartFile) { + if (multipartFile == null || multipartFile.isEmpty() || multipartFile.getSize() == 0) { + throw new CustomException(HttpStatus.NOT_FOUND, ErrorType.INVALID_FILE); + } + } + + public static boolean isFileNull(MultipartFile multipartFile) { + if (multipartFile == null || multipartFile.isEmpty() || multipartFile.getSize() == 0) { + return true; + } + + return false; + } +} diff --git a/src/main/java/org/jullaene/walkmong_back/config/S3Config.java b/src/main/java/org/jullaene/walkmong_back/config/S3Config.java new file mode 100644 index 0000000..5750188 --- /dev/null +++ b/src/main/java/org/jullaene/walkmong_back/config/S3Config.java @@ -0,0 +1,32 @@ +package org.jullaene.walkmong_back.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() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } +}