From fc847fc37076ee1859ca96eb3316d50e0f4f88a6 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 14:40:20 +0900 Subject: [PATCH 01/37] Feat(#54): member extends baseTimeEntity --- .../com/numberone/backend/domain/member/entity/Member.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/numberone/backend/domain/member/entity/Member.java b/src/main/java/com/numberone/backend/domain/member/entity/Member.java index adfe9936..f5166d34 100644 --- a/src/main/java/com/numberone/backend/domain/member/entity/Member.java +++ b/src/main/java/com/numberone/backend/domain/member/entity/Member.java @@ -1,5 +1,10 @@ package com.numberone.backend.domain.member.entity; +import com.numberone.backend.config.basetime.BaseTimeEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import com.numberone.backend.domain.notificationdisaster.entity.NotificationDisaster; import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; import com.numberone.backend.domain.support.entity.Support; @@ -16,7 +21,7 @@ @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -public class Member { +public class Member extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; From e7cde18f886327440f7adc7700d15c72fb936481 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 14:40:55 +0900 Subject: [PATCH 02/37] Feat(#54): implement SecurityContextProvider - for parsing principal from jwt token --- .../token/util/SecurityContextProvider.java | 19 +++++++++++++++++++ .../BadUserAuthenticationException.java | 9 +++++++++ .../context/CustomExceptionContext.java | 1 + 3 files changed, 29 insertions(+) create mode 100644 src/main/java/com/numberone/backend/domain/token/util/SecurityContextProvider.java create mode 100644 src/main/java/com/numberone/backend/exception/badrequest/BadUserAuthenticationException.java diff --git a/src/main/java/com/numberone/backend/domain/token/util/SecurityContextProvider.java b/src/main/java/com/numberone/backend/domain/token/util/SecurityContextProvider.java new file mode 100644 index 00000000..42a23e75 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/token/util/SecurityContextProvider.java @@ -0,0 +1,19 @@ +package com.numberone.backend.domain.token.util; + +import com.numberone.backend.exception.badrequest.BadUserAuthenticationException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Objects; + +public class SecurityContextProvider { + + public static String getAuthenticatedUserEmail(){ + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Object principal = authentication.getPrincipal(); + if (Objects.isNull(principal)){ + throw new BadUserAuthenticationException(); + } + return (String) principal; + } +} diff --git a/src/main/java/com/numberone/backend/exception/badrequest/BadUserAuthenticationException.java b/src/main/java/com/numberone/backend/exception/badrequest/BadUserAuthenticationException.java new file mode 100644 index 00000000..b11d1719 --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/badrequest/BadUserAuthenticationException.java @@ -0,0 +1,9 @@ +package com.numberone.backend.exception.badrequest; + +import static com.numberone.backend.exception.context.CustomExceptionContext.BAD_USER_AUTHENTICATION; + +public class BadUserAuthenticationException extends BadRequestException { + public BadUserAuthenticationException(){ + super(BAD_USER_AUTHENTICATION); + } +} diff --git a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java index e77d699d..678519b3 100644 --- a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java +++ b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java @@ -14,6 +14,7 @@ public enum CustomExceptionContext implements ExceptionContext { EXPIRED_ACCESS_TOKEN("만료된 액세스 토큰입니다. 리프레쉬 토큰을 이용하여 갱신해주세요.", 2002), WRONG_REFRESH_TOKEN("존재하지 않거나 만료된 리프레쉬 토큰입니다. 다시 리프레쉬 토큰을 발급받아주세요.", 2003), BAD_REQUEST_SOCIAL_TOKEN("요청하신 네이버 또는 카카오 소셜 토큰이 유효하지 않습니다.", 2004), + BAD_USER_AUTHENTICATION("해당 토큰의 인증 정보가 유효하지 않습니다.", 2005), // SHELTER 관련 예외 NOT_FOUND_SHELTER("주변에 가까운 대피소가 존재하지 않습니다.", 3000), From 4bfd568fa0970e5839adbe2f752a6e57d80b3687 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 17:29:05 +0900 Subject: [PATCH 03/37] =?UTF-8?q?Feat(#54):=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84,=20=EB=8B=89=EB=84=A4=EC=9E=84,=20=EC=8B=A4=EB=AA=85,?= =?UTF-8?q?=20fcm=20=ED=86=A0=ED=81=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/member/entity/Member.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/com/numberone/backend/domain/member/entity/Member.java b/src/main/java/com/numberone/backend/domain/member/entity/Member.java index f5166d34..299296f7 100644 --- a/src/main/java/com/numberone/backend/domain/member/entity/Member.java +++ b/src/main/java/com/numberone/backend/domain/member/entity/Member.java @@ -1,6 +1,7 @@ package com.numberone.backend.domain.member.entity; import com.numberone.backend.config.basetime.BaseTimeEntity; +import jakarta.persistence.*; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -18,9 +19,11 @@ import java.util.ArrayList; import java.util.List; +@Comment("회원 정보") @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter +@Table(name = "MEMBER") public class Member extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -41,6 +44,12 @@ public class Member extends BaseTimeEntity { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List supports = new ArrayList<>(); + @Comment("회원 프로필 사진 URL") + private String profileImageUrl; + + @Comment("FCM 토큰") + private String fcmToken; + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List notificationDisasters = new ArrayList<>(); @@ -63,6 +72,10 @@ public static Member of(String email, String realName) { .build(); } + public void updateProfileImageUrl(String imageUrl){ + this.profileImageUrl = imageUrl; + } + public void updateNickname(String nickname) { this.nickName = nickname; } From b135913c982d05fba13b6ecdb2a14eaa51802aaa Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 17:29:32 +0900 Subject: [PATCH 04/37] =?UTF-8?q?Feat(#54):=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=82=AC=EC=A7=84=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 10 +++++++++ .../response/UploadProfileImageResponse.java | 19 ++++++++++++++++ .../domain/member/service/MemberService.java | 22 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 src/main/java/com/numberone/backend/domain/member/dto/response/UploadProfileImageResponse.java diff --git a/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java b/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java index c8609d97..44b67984 100644 --- a/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java +++ b/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java @@ -3,6 +3,7 @@ import com.numberone.backend.domain.member.dto.request.OnboardingRequest; import com.numberone.backend.domain.member.dto.request.BuyHeartRequest; import com.numberone.backend.domain.member.dto.response.HeartCntResponse; +import com.numberone.backend.domain.member.dto.response.UploadProfileImageResponse; import com.numberone.backend.domain.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -18,6 +19,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URI; @Tag(name = "members", description = "사용자 관련 API") @RestController @@ -26,6 +30,12 @@ public class MemberController { private final MemberService memberService; + @PostMapping("/profile-image") + public ResponseEntity uploadMemberProfileImage(@RequestPart MultipartFile image){ + return ResponseEntity.created(URI.create("/api/members/profile-image")) + .body(memberService.uploadProfileImage(image)); + } + @PostMapping("/heart") @Operation(summary = "마음 구입하기", description = """ 구입한 마음 갯수를 body에 담아 전달해주세요. diff --git a/src/main/java/com/numberone/backend/domain/member/dto/response/UploadProfileImageResponse.java b/src/main/java/com/numberone/backend/domain/member/dto/response/UploadProfileImageResponse.java new file mode 100644 index 00000000..31992a5b --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/member/dto/response/UploadProfileImageResponse.java @@ -0,0 +1,19 @@ +package com.numberone.backend.domain.member.dto.response; + +import lombok.*; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UploadProfileImageResponse { + + private String imageUrl; + + public static UploadProfileImageResponse of(String imageUrl) { + return UploadProfileImageResponse.builder() + .imageUrl(imageUrl) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java index 4f222276..82521e67 100644 --- a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java +++ b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java @@ -1,5 +1,6 @@ package com.numberone.backend.domain.member.service; +import com.numberone.backend.domain.member.dto.response.UploadProfileImageResponse; import com.numberone.backend.domain.disaster.util.DisasterType; import com.numberone.backend.domain.member.dto.request.OnboardingAddress; import com.numberone.backend.domain.member.dto.request.OnboardingDisasterType; @@ -8,20 +9,27 @@ import com.numberone.backend.domain.member.dto.response.HeartCntResponse; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; +import com.numberone.backend.domain.token.util.SecurityContextProvider; import com.numberone.backend.domain.notificationdisaster.entity.NotificationDisaster; import com.numberone.backend.domain.notificationdisaster.repository.NotificationDisasterRepository; import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; import com.numberone.backend.domain.notificationregion.repository.NotificationRegionRepository; import com.numberone.backend.exception.notfound.NotFoundMemberException; +import com.numberone.backend.support.S3Provider; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @RequiredArgsConstructor @Service +@Slf4j @Transactional(readOnly = true) public class MemberService { private final MemberRepository memberRepository; + private final S3Provider s3Provider; private final NotificationDisasterRepository notificationDisasterRepository; private final NotificationRegionRepository notificationRegionRepository; @@ -71,4 +79,18 @@ public HeartCntResponse getHeart(String email) { .orElseThrow(NotFoundMemberException::new); return HeartCntResponse.of(member); } + + @Transactional + public UploadProfileImageResponse uploadProfileImage(MultipartFile image){ + String email = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(email) + .orElseThrow(NotFoundMemberException::new); + String imageUrl = s3Provider.uploadImage(image); + + log.info("[회원의 프로필 이미지를 업로드하였습니다.] id:{} url:{}", member.getId(), imageUrl); + + member.updateProfileImageUrl(imageUrl); + + return UploadProfileImageResponse.of(imageUrl); + } } From 6ce83d4f789e4ad854f5a3bfebf8c647760becff Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 17:31:57 +0900 Subject: [PATCH 05/37] =?UTF-8?q?Feat(#54):=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?-=20Article:=20=EB=8F=99=EB=84=A4=EC=83=9D=ED=99=9C=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20-=20ArticleTag:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=ED=83=9C=EA=B7=B8=20-=20CommentEntity:=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20-=20CommunityParticipant:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=B0=B8=EC=97=AC=EC=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/entity/Article.java | 47 +++++++++++++++++++ .../domain/article/entity/ArticleTag.java | 12 +++++ .../domain/comment/entity/CommentEntity.java | 31 ++++++++++++ .../entity/CommunityParticipant.java | 30 ++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 src/main/java/com/numberone/backend/domain/article/entity/Article.java create mode 100644 src/main/java/com/numberone/backend/domain/article/entity/ArticleTag.java create mode 100644 src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java create mode 100644 src/main/java/com/numberone/backend/domain/communityparticipant/entity/CommunityParticipant.java diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java new file mode 100644 index 00000000..e047497a --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -0,0 +1,47 @@ +package com.numberone.backend.domain.article.entity; + +import com.numberone.backend.config.basetime.BaseTimeEntity; +import com.numberone.backend.domain.comment.entity.CommentEntity; +import com.numberone.backend.domain.communityparticipant.entity.CommunityParticipant; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +import java.util.ArrayList; +import java.util.List; + +@Comment("동네생활 게시글 정보") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +@Table(name = "ARTICLE") +public class Article extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "article_id") + private Long id; + + @OneToMany(mappedBy = "article", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @OneToMany(mappedBy = "article", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) + private List communityParticipants = new ArrayList<>(); + + @Comment("게시글 제목") + private String title; + + @Comment("게시글 내용") + private String content; + + @Comment("게시글 태그 (일상:LIFE, 사기:FRAUD, 치안:SAFETY, 제보:REPORT)") + @Enumerated(EnumType.STRING) + private ArticleTag articleTag; + + @Comment("게시글 작성 당시 주소") + private String address; + + @Comment("게시글 좋아요 개수") + private Integer likeCount; // todo: 동시성 처리 +} diff --git a/src/main/java/com/numberone/backend/domain/article/entity/ArticleTag.java b/src/main/java/com/numberone/backend/domain/article/entity/ArticleTag.java new file mode 100644 index 00000000..1ebf82a4 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/entity/ArticleTag.java @@ -0,0 +1,12 @@ +package com.numberone.backend.domain.article.entity; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum ArticleTag { + LIFE, // 일상 + FRAUD, // 사기 + SAFETY, // 치안 + REPORT; // 제보 + private String value; +} diff --git a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java new file mode 100644 index 00000000..ff922c1f --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java @@ -0,0 +1,31 @@ +package com.numberone.backend.domain.comment.entity; + +import com.numberone.backend.config.basetime.BaseTimeEntity; +import com.numberone.backend.domain.article.entity.Article; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Comment("동네생활 댓글 정보") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +@Table(name = "COMMENT") +public class CommentEntity extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinColumn(name = "article_id") + private Article article; + + @Comment("댓글 depth {0: 댓글, 1: 대댓글}") + private Integer depth; + + @Comment("댓글 좋아요 개수") + private Integer likeCount; // todo: 동시성 처리 +} diff --git a/src/main/java/com/numberone/backend/domain/communityparticipant/entity/CommunityParticipant.java b/src/main/java/com/numberone/backend/domain/communityparticipant/entity/CommunityParticipant.java new file mode 100644 index 00000000..d48fb487 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/communityparticipant/entity/CommunityParticipant.java @@ -0,0 +1,30 @@ +package com.numberone.backend.domain.communityparticipant.entity; + +import com.numberone.backend.config.basetime.BaseTimeEntity; +import com.numberone.backend.domain.article.entity.Article; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Comment("동네생활 게시글 참여자") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +@Table(name = "COMMUNITY_PARTICIPANT") +public class CommunityParticipant extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "community_participant_id") + private Long id; + + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinColumn(name = "article_id") + private Article article; + + @Comment("해당 게시글의 작성자이면 true") + private Boolean isOwner; +} From 181389a9f9ed2c235bafdc572b9b992d23e2f35d Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 17:35:15 +0900 Subject: [PATCH 06/37] =?UTF-8?q?Docs(#54):=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=82=AC=EC=A7=84=20=EC=97=85=EB=A1=9C=EB=93=9C=20api=20-?= =?UTF-8?q?=20swagger=20api=20summary,=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/controller/MemberController.java | 12 ++++++++++-- .../backend/domain/member/service/MemberService.java | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java b/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java index 44b67984..64e7cea8 100644 --- a/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java +++ b/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java @@ -6,9 +6,11 @@ import com.numberone.backend.domain.member.dto.response.UploadProfileImageResponse; import com.numberone.backend.domain.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -23,15 +25,21 @@ import java.net.URI; +@Slf4j @Tag(name = "members", description = "사용자 관련 API") @RestController @RequiredArgsConstructor -@RequestMapping("/api/members") public class MemberController { private final MemberService memberService; + @Operation(summary = "회원 프로필 사진 업로드 API", + description = """ + 1. 반드시 access token 을 헤더에 포함하여 호출해주세요. (유저를 식별하기 위함입니다.) + + 2. 프로필 사진은 MultipartFile 으로 반드시 image 라는 이름으로 보내주세요 + """) @PostMapping("/profile-image") - public ResponseEntity uploadMemberProfileImage(@RequestPart MultipartFile image){ + public ResponseEntity uploadMemberProfileImage(@RequestPart("image") MultipartFile image) { return ResponseEntity.created(URI.create("/api/members/profile-image")) .body(memberService.uploadProfileImage(image)); } diff --git a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java index 82521e67..68224b4f 100644 --- a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java +++ b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java @@ -93,4 +93,5 @@ public UploadProfileImageResponse uploadProfileImage(MultipartFile image){ return UploadProfileImageResponse.of(imageUrl); } + } From 609d5178f227e2b396a907d73221a0b511cb2b9f Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 17:38:15 +0900 Subject: [PATCH 07/37] Feat(#54): relocate s3Provider --- .../numberone/backend/domain/admin/service/AdminService.java | 2 +- .../numberone/backend/domain/member/service/MemberService.java | 2 +- .../backend/domain/shelter/service/ShelterService.java | 2 +- .../java/com/numberone/backend/support/{ => s3}/S3Provider.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/main/java/com/numberone/backend/support/{ => s3}/S3Provider.java (98%) diff --git a/src/main/java/com/numberone/backend/domain/admin/service/AdminService.java b/src/main/java/com/numberone/backend/domain/admin/service/AdminService.java index 11604792..5424cb8c 100644 --- a/src/main/java/com/numberone/backend/domain/admin/service/AdminService.java +++ b/src/main/java/com/numberone/backend/domain/admin/service/AdminService.java @@ -4,7 +4,7 @@ import com.numberone.backend.domain.admin.dto.response.GetAddressResponse; import com.numberone.backend.domain.shelter.dto.response.GetAllSheltersResponse; import com.numberone.backend.domain.shelter.repository.ShelterRepository; -import com.numberone.backend.support.S3Provider; +import com.numberone.backend.support.s3.S3Provider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java index 68224b4f..03d41210 100644 --- a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java +++ b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java @@ -15,7 +15,7 @@ import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; import com.numberone.backend.domain.notificationregion.repository.NotificationRegionRepository; import com.numberone.backend.exception.notfound.NotFoundMemberException; -import com.numberone.backend.support.S3Provider; +import com.numberone.backend.support.s3.S3Provider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.context.SecurityContextHolder; diff --git a/src/main/java/com/numberone/backend/domain/shelter/service/ShelterService.java b/src/main/java/com/numberone/backend/domain/shelter/service/ShelterService.java index 21e1afd7..d8b96dfc 100644 --- a/src/main/java/com/numberone/backend/domain/shelter/service/ShelterService.java +++ b/src/main/java/com/numberone/backend/domain/shelter/service/ShelterService.java @@ -5,7 +5,7 @@ import com.numberone.backend.domain.shelter.repository.ShelterRepository; import com.numberone.backend.domain.shelter.util.ShelterType; import com.numberone.backend.exception.notfound.NotFoundShelterException; -import com.numberone.backend.support.S3Provider; +import com.numberone.backend.support.s3.S3Provider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/numberone/backend/support/S3Provider.java b/src/main/java/com/numberone/backend/support/s3/S3Provider.java similarity index 98% rename from src/main/java/com/numberone/backend/support/S3Provider.java rename to src/main/java/com/numberone/backend/support/s3/S3Provider.java index 128054f5..c76c82f2 100644 --- a/src/main/java/com/numberone/backend/support/S3Provider.java +++ b/src/main/java/com/numberone/backend/support/s3/S3Provider.java @@ -1,4 +1,4 @@ -package com.numberone.backend.support; +package com.numberone.backend.support.s3; import com.amazonaws.HttpMethod; import com.amazonaws.services.s3.AmazonS3Client; From 85b334cd5438a8b136d219d3ea0a4d8417a9ba5e Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 17:41:16 +0900 Subject: [PATCH 08/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80,?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80,=20=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0?= =?UTF-8?q?=20=EC=B0=B8=EC=97=AC=EC=9E=90=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/repository/ArticleRepository.java | 7 +++++++ .../domain/comment/repository/CommentRepository.java | 7 +++++++ .../repository/CommunityParticipantRepository.java | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 src/main/java/com/numberone/backend/domain/article/repository/ArticleRepository.java create mode 100644 src/main/java/com/numberone/backend/domain/comment/repository/CommentRepository.java create mode 100644 src/main/java/com/numberone/backend/domain/communityparticipant/repository/CommunityParticipantRepository.java diff --git a/src/main/java/com/numberone/backend/domain/article/repository/ArticleRepository.java b/src/main/java/com/numberone/backend/domain/article/repository/ArticleRepository.java new file mode 100644 index 00000000..3f2d5489 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/repository/ArticleRepository.java @@ -0,0 +1,7 @@ +package com.numberone.backend.domain.article.repository; + +import com.numberone.backend.domain.article.entity.Article; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleRepository extends JpaRepository { +} diff --git a/src/main/java/com/numberone/backend/domain/comment/repository/CommentRepository.java b/src/main/java/com/numberone/backend/domain/comment/repository/CommentRepository.java new file mode 100644 index 00000000..d86de378 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package com.numberone.backend.domain.comment.repository; + +import com.numberone.backend.domain.comment.entity.CommentEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} diff --git a/src/main/java/com/numberone/backend/domain/communityparticipant/repository/CommunityParticipantRepository.java b/src/main/java/com/numberone/backend/domain/communityparticipant/repository/CommunityParticipantRepository.java new file mode 100644 index 00000000..b9a1bd04 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/communityparticipant/repository/CommunityParticipantRepository.java @@ -0,0 +1,7 @@ +package com.numberone.backend.domain.communityparticipant.repository; + +import com.numberone.backend.domain.communityparticipant.entity.CommunityParticipant; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommunityParticipantRepository extends JpaRepository { +} From 1730c5bd1f1c4c5e4fe32dfde9719d96326460ef Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 17:44:30 +0900 Subject: [PATCH 09/37] Feat(#54): rename CommunityParticipant - to ArticleParticipant --- .../entity/QCommunityParticipant.java | 61 +++++++++++++++++++ .../domain/article/entity/Article.java | 4 +- .../entity/ArticleParticipant.java} | 8 +-- .../ArticleParticipantRepository.java | 7 +++ .../CommunityParticipantRepository.java | 7 --- 5 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 src/main/generated/com/numberone/backend/domain/articleparticipant/entity/QCommunityParticipant.java rename src/main/java/com/numberone/backend/domain/{communityparticipant/entity/CommunityParticipant.java => articleparticipant/entity/ArticleParticipant.java} (76%) create mode 100644 src/main/java/com/numberone/backend/domain/articleparticipant/repository/ArticleParticipantRepository.java delete mode 100644 src/main/java/com/numberone/backend/domain/communityparticipant/repository/CommunityParticipantRepository.java diff --git a/src/main/generated/com/numberone/backend/domain/articleparticipant/entity/QCommunityParticipant.java b/src/main/generated/com/numberone/backend/domain/articleparticipant/entity/QCommunityParticipant.java new file mode 100644 index 00000000..c00659b5 --- /dev/null +++ b/src/main/generated/com/numberone/backend/domain/articleparticipant/entity/QCommunityParticipant.java @@ -0,0 +1,61 @@ +package com.numberone.backend.domain.articleparticipant.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QCommunityParticipant is a Querydsl query type for CommunityParticipant + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QCommunityParticipant extends EntityPathBase { + + private static final long serialVersionUID = -1555403901L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QCommunityParticipant communityParticipant = new QCommunityParticipant("communityParticipant"); + + public final com.numberone.backend.config.basetime.QBaseTimeEntity _super = new com.numberone.backend.config.basetime.QBaseTimeEntity(this); + + public final com.numberone.backend.domain.article.entity.QArticle article; + + //inherited + public final DateTimePath createdAt = _super.createdAt; + + public final NumberPath id = createNumber("id", Long.class); + + public final BooleanPath isOwner = createBoolean("isOwner"); + + //inherited + public final DateTimePath modifiedAt = _super.modifiedAt; + + public QCommunityParticipant(String variable) { + this(ArticleParticipant.class, forVariable(variable), INITS); + } + + public QCommunityParticipant(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QCommunityParticipant(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QCommunityParticipant(PathMetadata metadata, PathInits inits) { + this(ArticleParticipant.class, metadata, inits); + } + + public QCommunityParticipant(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.article = inits.isInitialized("article") ? new com.numberone.backend.domain.article.entity.QArticle(forProperty("article")) : null; + } + +} + diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index e047497a..8de22d20 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -2,7 +2,7 @@ import com.numberone.backend.config.basetime.BaseTimeEntity; import com.numberone.backend.domain.comment.entity.CommentEntity; -import com.numberone.backend.domain.communityparticipant.entity.CommunityParticipant; +import com.numberone.backend.domain.articleparticipant.entity.ArticleParticipant; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -27,7 +27,7 @@ public class Article extends BaseTimeEntity { private List comments = new ArrayList<>(); @OneToMany(mappedBy = "article", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) - private List communityParticipants = new ArrayList<>(); + private List articleParticipants = new ArrayList<>(); @Comment("게시글 제목") private String title; diff --git a/src/main/java/com/numberone/backend/domain/communityparticipant/entity/CommunityParticipant.java b/src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java similarity index 76% rename from src/main/java/com/numberone/backend/domain/communityparticipant/entity/CommunityParticipant.java rename to src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java index d48fb487..ae868e31 100644 --- a/src/main/java/com/numberone/backend/domain/communityparticipant/entity/CommunityParticipant.java +++ b/src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java @@ -1,4 +1,4 @@ -package com.numberone.backend.domain.communityparticipant.entity; +package com.numberone.backend.domain.articleparticipant.entity; import com.numberone.backend.config.basetime.BaseTimeEntity; import com.numberone.backend.domain.article.entity.Article; @@ -12,12 +12,12 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Entity -@Table(name = "COMMUNITY_PARTICIPANT") -public class CommunityParticipant extends BaseTimeEntity { +@Table(name = "ARTICLE_PARTICIPANT") +public class ArticleParticipant extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "community_participant_id") + @Column(name = "article_participant_id") private Long id; diff --git a/src/main/java/com/numberone/backend/domain/articleparticipant/repository/ArticleParticipantRepository.java b/src/main/java/com/numberone/backend/domain/articleparticipant/repository/ArticleParticipantRepository.java new file mode 100644 index 00000000..6e57bde9 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/articleparticipant/repository/ArticleParticipantRepository.java @@ -0,0 +1,7 @@ +package com.numberone.backend.domain.articleparticipant.repository; + +import com.numberone.backend.domain.articleparticipant.entity.ArticleParticipant; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleParticipantRepository extends JpaRepository { +} diff --git a/src/main/java/com/numberone/backend/domain/communityparticipant/repository/CommunityParticipantRepository.java b/src/main/java/com/numberone/backend/domain/communityparticipant/repository/CommunityParticipantRepository.java deleted file mode 100644 index b9a1bd04..00000000 --- a/src/main/java/com/numberone/backend/domain/communityparticipant/repository/CommunityParticipantRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.numberone.backend.domain.communityparticipant.repository; - -import com.numberone.backend.domain.communityparticipant.entity.CommunityParticipant; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CommunityParticipantRepository extends JpaRepository { -} From 3a711c45ccdaf344f9ddc05355e4ef60f0a58959 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 19:52:36 +0900 Subject: [PATCH 10/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 30 +++++++ .../dto/request/UploadArticleRequest.java | 35 ++++++++ .../dto/response/UploadArticleResponse.java | 32 +++++++ .../domain/article/entity/Article.java | 29 ++++++- .../article/service/ArticleService.java | 86 +++++++++++++++++++ .../articleimage/entity/ArticleImage.java | 33 +++++++ .../repository/ArticleImageRepository.java | 7 ++ .../entity/ArticleParticipant.java | 9 +- 8 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java create mode 100644 src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java create mode 100644 src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java create mode 100644 src/main/java/com/numberone/backend/domain/article/service/ArticleService.java create mode 100644 src/main/java/com/numberone/backend/domain/articleimage/entity/ArticleImage.java create mode 100644 src/main/java/com/numberone/backend/domain/articleimage/repository/ArticleImageRepository.java diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java new file mode 100644 index 00000000..4bc6e639 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -0,0 +1,30 @@ +package com.numberone.backend.domain.article.controller; + +import com.numberone.backend.domain.article.dto.request.UploadArticleRequest; +import com.numberone.backend.domain.article.dto.response.UploadArticleResponse; +import com.numberone.backend.domain.article.service.ArticleService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; + +@Slf4j +@RequestMapping("/api/articles") +@RequiredArgsConstructor +@RestController +public class ArticleController { + private final ArticleService articleService; + + @PostMapping + public ResponseEntity uploadArticle(@RequestBody @Valid UploadArticleRequest request){ + return ResponseEntity.created(URI.create("/api/articles")) + .body(articleService.uploadArticle(request)); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java b/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java new file mode 100644 index 00000000..02aa22c8 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java @@ -0,0 +1,35 @@ +package com.numberone.backend.domain.article.dto.request; + +import com.numberone.backend.domain.article.entity.ArticleTag; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UploadArticleRequest { + + // 글 관련 + @NotNull(message = "글 제목은 null 일 수 없습니다.") + private String title; // 제목 + + @NotNull(message = "내용은 null 일 수 없습니다.") + private String content; // 내용 + + @NotNull(message = """ + 게시글의 태그를 하나 선택해주세요. + + LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보) + """) + private ArticleTag articleTag; // 게시글 태그 + + // 이미지 관련 + private List imageList; // 이미지 리스트 + private Long thumbNailImageIdx; // 썸네일 이미지의 순서 (0,1,2,...) + +} diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java new file mode 100644 index 00000000..0699dd9e --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java @@ -0,0 +1,32 @@ +package com.numberone.backend.domain.article.dto.response; + +import com.numberone.backend.domain.article.entity.Article; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UploadArticleResponse { + + private Long articleId; + private LocalDateTime createdAt; + + // 이미지 관련 + private List imageUrls; + private String thumbNailImageUrl; + + public static UploadArticleResponse of(Article article, List imageUrls, String thumbNailImageUrl){ + return UploadArticleResponse.builder() + .articleId(article.getId()) + .createdAt(article.getCreatedAt()) + .imageUrls(imageUrls) + .thumbNailImageUrl(thumbNailImageUrl) + .build(); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index 8de22d20..c14db2ee 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -1,8 +1,9 @@ package com.numberone.backend.domain.article.entity; import com.numberone.backend.config.basetime.BaseTimeEntity; -import com.numberone.backend.domain.comment.entity.CommentEntity; +import com.numberone.backend.domain.articleimage.entity.ArticleImage; import com.numberone.backend.domain.articleparticipant.entity.ArticleParticipant; +import com.numberone.backend.domain.comment.entity.CommentEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -23,12 +24,18 @@ public class Article extends BaseTimeEntity { @Column(name = "article_id") private Long id; - @OneToMany(mappedBy = "article", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) + @OneToMany(mappedBy = "article", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) private List comments = new ArrayList<>(); - @OneToMany(mappedBy = "article", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) + @OneToMany(mappedBy = "article", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) private List articleParticipants = new ArrayList<>(); + @OneToMany(mappedBy = "article", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) + private List articleImages = new ArrayList<>(); + + @Comment("썸네일 이미지 url ID") + private Long thumbNailImageUrlId; + @Comment("게시글 제목") private String title; @@ -44,4 +51,20 @@ public class Article extends BaseTimeEntity { @Comment("게시글 좋아요 개수") private Integer likeCount; // todo: 동시성 처리 + + @Comment("작성자 ID") + private Long articleOwnerId; + + public Article(String title, String content, Long articleOwnerId, ArticleTag tag) { + this.title = title; + this.content = content; + this.articleOwnerId = articleOwnerId; + this.articleTag = tag; + } + + public void updateArticleImage(List images, Long thumbNailImageUrlId) { + this.articleImages = images; + this.thumbNailImageUrlId = thumbNailImageUrlId; + } + } diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java new file mode 100644 index 00000000..316cf53c --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -0,0 +1,86 @@ +package com.numberone.backend.domain.article.service; + +import com.numberone.backend.domain.article.dto.request.UploadArticleRequest; +import com.numberone.backend.domain.article.dto.response.UploadArticleResponse; +import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.article.repository.ArticleRepository; +import com.numberone.backend.domain.articleimage.entity.ArticleImage; +import com.numberone.backend.domain.articleimage.repository.ArticleImageRepository; +import com.numberone.backend.domain.articleparticipant.entity.ArticleParticipant; +import com.numberone.backend.domain.articleparticipant.repository.ArticleParticipantRepository; +import com.numberone.backend.domain.member.entity.Member; +import com.numberone.backend.domain.member.repository.MemberRepository; +import com.numberone.backend.domain.token.util.SecurityContextProvider; +import com.numberone.backend.exception.notfound.NotFoundMemberException; +import com.numberone.backend.support.s3.S3Provider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Slf4j +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class ArticleService { + + private final ArticleRepository articleRepository; + private final MemberRepository memberRepository; + private final ArticleParticipantRepository articleParticipantRepository; + private final ArticleImageRepository articleImageRepository; + private final S3Provider s3Provider; + + @Transactional + public UploadArticleResponse uploadArticle(UploadArticleRequest request) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member owner = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + + // 1. 게시글 생성 ( 제목, 내용, 작성자 아이디, 태그) + Article article = articleRepository.save( + new Article( + request.getTitle(), + request.getContent(), + owner.getId(), + request.getArticleTag()) + ); + articleParticipantRepository.save( + new ArticleParticipant(article, owner.getId()) + ); + + // 2. 이미지 업로드 + List articleImages = new ArrayList<>(); + List imageUrls = new ArrayList<>(); + String thumbNailImageUrl = ""; + Long thumbNailImageId = 1L; + if (!Objects.isNull(request.getImageList())) { + List imageList = request.getImageList(); + + for (int i = 0; i < imageList.size(); i++) { + String imageUrl = s3Provider.uploadImage(imageList.get(i)); + imageUrls.add(imageUrl); + + ArticleImage savedArticleImage = articleImageRepository.save( + new ArticleImage(article, imageUrl) + ); + articleImages.add(savedArticleImage); + if (Objects.equals(i, request.getThumbNailImageIdx())) { + thumbNailImageUrl = imageUrl; + thumbNailImageId = savedArticleImage.getId(); + } + + } + } + + // 3. 게시글 - 이미지 연관 관계 설정 + article.updateArticleImage(articleImages, thumbNailImageId); + + return UploadArticleResponse.of(article, imageUrls, thumbNailImageUrl); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/articleimage/entity/ArticleImage.java b/src/main/java/com/numberone/backend/domain/articleimage/entity/ArticleImage.java new file mode 100644 index 00000000..2fdae5f5 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/articleimage/entity/ArticleImage.java @@ -0,0 +1,33 @@ +package com.numberone.backend.domain.articleimage.entity; + +import com.numberone.backend.domain.article.entity.Article; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Comment("동네생활 게시글 이미지 정보") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +@Table(name = "ARTICLE_IMAGE") +public class ArticleImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinColumn(name = "article_id") + private Article article; + + @Comment("동네생활 게시글 이미지 URL") + private String imageUrl; + + + public ArticleImage(Article article, String imageUrl){ + this.article = article; + this.imageUrl = imageUrl; + } +} diff --git a/src/main/java/com/numberone/backend/domain/articleimage/repository/ArticleImageRepository.java b/src/main/java/com/numberone/backend/domain/articleimage/repository/ArticleImageRepository.java new file mode 100644 index 00000000..5a835d7f --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/articleimage/repository/ArticleImageRepository.java @@ -0,0 +1,7 @@ +package com.numberone.backend.domain.articleimage.repository; + +import com.numberone.backend.domain.articleimage.entity.ArticleImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleImageRepository extends JpaRepository { +} diff --git a/src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java b/src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java index ae868e31..92423f2a 100644 --- a/src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java +++ b/src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java @@ -25,6 +25,11 @@ public class ArticleParticipant extends BaseTimeEntity { @JoinColumn(name = "article_id") private Article article; - @Comment("해당 게시글의 작성자이면 true") - private Boolean isOwner; + @Comment("회원 id") + private Long memberId; + + public ArticleParticipant(Article article, Long memberId){ + this.article = article; + this.memberId= memberId; + } } From e4b0c0d6a3801541b3c17888073850022e3a2327 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 19:59:30 +0900 Subject: [PATCH 11/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=ED=99=9C=EC=84=B1=ED=99=94=20=EC=83=81=ED=83=9C=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/dto/response/UploadArticleResponse.java | 4 ++++ .../backend/domain/article/entity/Article.java | 5 +++++ .../backend/domain/article/entity/ArticleStatus.java | 10 ++++++++++ 3 files changed, 19 insertions(+) create mode 100644 src/main/java/com/numberone/backend/domain/article/entity/ArticleStatus.java diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java index 0699dd9e..f74e51a8 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java @@ -20,12 +20,16 @@ public class UploadArticleResponse { private List imageUrls; private String thumbNailImageUrl; + // 작성자 주소 + private String address; // todo: 더미 데이터 + public static UploadArticleResponse of(Article article, List imageUrls, String thumbNailImageUrl){ return UploadArticleResponse.builder() .articleId(article.getId()) .createdAt(article.getCreatedAt()) .imageUrls(imageUrls) .thumbNailImageUrl(thumbNailImageUrl) + .address("서울시 광진구 자양동") .build(); } diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index c14db2ee..b26242cf 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -46,6 +46,10 @@ public class Article extends BaseTimeEntity { @Enumerated(EnumType.STRING) private ArticleTag articleTag; + @Comment("게시글 상태 (ACTIVATED, DELETED)") + @Enumerated(EnumType.STRING) + private ArticleStatus articleStatus; + @Comment("게시글 작성 당시 주소") private String address; @@ -60,6 +64,7 @@ public Article(String title, String content, Long articleOwnerId, ArticleTag tag this.content = content; this.articleOwnerId = articleOwnerId; this.articleTag = tag; + this.articleStatus = ArticleStatus.ACTIVATED; } public void updateArticleImage(List images, Long thumbNailImageUrlId) { diff --git a/src/main/java/com/numberone/backend/domain/article/entity/ArticleStatus.java b/src/main/java/com/numberone/backend/domain/article/entity/ArticleStatus.java new file mode 100644 index 00000000..97e66db9 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/entity/ArticleStatus.java @@ -0,0 +1,10 @@ +package com.numberone.backend.domain.article.entity; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum ArticleStatus { + ACTIVATED, + DELETED; + private String value; +} From eabb6e68ca14fef812571eab7fe8b76dd06932b6 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 20:00:47 +0900 Subject: [PATCH 12/37] Feat(#54): removed Qclass --- .../entity/QCommunityParticipant.java | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 src/main/generated/com/numberone/backend/domain/articleparticipant/entity/QCommunityParticipant.java diff --git a/src/main/generated/com/numberone/backend/domain/articleparticipant/entity/QCommunityParticipant.java b/src/main/generated/com/numberone/backend/domain/articleparticipant/entity/QCommunityParticipant.java deleted file mode 100644 index c00659b5..00000000 --- a/src/main/generated/com/numberone/backend/domain/articleparticipant/entity/QCommunityParticipant.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.numberone.backend.domain.articleparticipant.entity; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QCommunityParticipant is a Querydsl query type for CommunityParticipant - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QCommunityParticipant extends EntityPathBase { - - private static final long serialVersionUID = -1555403901L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QCommunityParticipant communityParticipant = new QCommunityParticipant("communityParticipant"); - - public final com.numberone.backend.config.basetime.QBaseTimeEntity _super = new com.numberone.backend.config.basetime.QBaseTimeEntity(this); - - public final com.numberone.backend.domain.article.entity.QArticle article; - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final BooleanPath isOwner = createBoolean("isOwner"); - - //inherited - public final DateTimePath modifiedAt = _super.modifiedAt; - - public QCommunityParticipant(String variable) { - this(ArticleParticipant.class, forVariable(variable), INITS); - } - - public QCommunityParticipant(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QCommunityParticipant(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QCommunityParticipant(PathMetadata metadata, PathInits inits) { - this(ArticleParticipant.class, metadata, inits); - } - - public QCommunityParticipant(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.article = inits.isInitialized("article") ? new com.numberone.backend.domain.article.entity.QArticle(forProperty("article")) : null; - } - -} - From 0db035ada77df82fc2157aac2348c3ab167b8ba9 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 20:08:37 +0900 Subject: [PATCH 13/37] =?UTF-8?q?Docs(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index 4bc6e639..465000c9 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -3,14 +3,12 @@ import com.numberone.backend.domain.article.dto.request.UploadArticleRequest; import com.numberone.backend.domain.article.dto.response.UploadArticleResponse; import com.numberone.backend.domain.article.service.ArticleService; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.net.URI; @@ -21,6 +19,22 @@ public class ArticleController { private final ArticleService articleService; + + @Operation(summary = "게시글 작성 API", description = """ + + 동네생활 게시글 등록 api 입니다. + 반드시 access token 을 헤더에 담아서 요청해주세요. + + 1. title 은 글 제목 입니다 (not null) + 2. content 는 글 내용 입니다 (not null) + 3. articleTag 는 게시글 태그 입니다. LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보) + 4. imageList 는 이미지 (MultiPart) 리스트 입니다. + 5. thumbNailImageIdx 는 썸네일 이미지의 인덱스 입니다. (0,1,2, ... + imageList 에 이미지를 담아서 보내는 경우, + idx 에 따라서 썸네일 이미지를 결정합니다. + + """) + @PostMapping public ResponseEntity uploadArticle(@RequestBody @Valid UploadArticleRequest request){ return ResponseEntity.created(URI.create("/api/articles")) From ef21a5422b02fdf068f7bcf6ec5c8ada93de6e2f Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 20:45:17 +0900 Subject: [PATCH 14/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 30 ++++++++++-- .../dto/response/DeleteArticleResponse.java | 24 ++++++++++ .../response/GetArticleDetailResponse.java | 48 +++++++++++++++++++ .../domain/article/entity/Article.java | 4 ++ .../article/service/ArticleService.java | 37 ++++++++++++++ .../repository/ArticleImageRepository.java | 5 ++ .../context/CustomExceptionContext.java | 8 +++- .../notfound/NotFoundArticleException.java | 9 ++++ .../NotFoundArticleImageException.java | 9 ++++ 9 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/article/dto/response/DeleteArticleResponse.java create mode 100644 src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java create mode 100644 src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleException.java create mode 100644 src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleImageException.java diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index 465000c9..d88995bf 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -1,10 +1,13 @@ package com.numberone.backend.domain.article.controller; import com.numberone.backend.domain.article.dto.request.UploadArticleRequest; +import com.numberone.backend.domain.article.dto.response.DeleteArticleResponse; +import com.numberone.backend.domain.article.dto.response.GetArticleDetailResponse; import com.numberone.backend.domain.article.dto.response.UploadArticleResponse; import com.numberone.backend.domain.article.service.ArticleService; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; +import jakarta.websocket.server.PathParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -17,14 +20,15 @@ @RequiredArgsConstructor @RestController public class ArticleController { + private final ArticleService articleService; @Operation(summary = "게시글 작성 API", description = """ - + 동네생활 게시글 등록 api 입니다. 반드시 access token 을 헤더에 담아서 요청해주세요. - + 1. title 은 글 제목 입니다 (not null) 2. content 는 글 내용 입니다 (not null) 3. articleTag 는 게시글 태그 입니다. LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보) @@ -32,13 +36,31 @@ public class ArticleController { 5. thumbNailImageIdx 는 썸네일 이미지의 인덱스 입니다. (0,1,2, ... imageList 에 이미지를 담아서 보내는 경우, idx 에 따라서 썸네일 이미지를 결정합니다. - + """) @PostMapping - public ResponseEntity uploadArticle(@RequestBody @Valid UploadArticleRequest request){ + public ResponseEntity uploadArticle(@RequestBody @Valid UploadArticleRequest request) { return ResponseEntity.created(URI.create("/api/articles")) .body(articleService.uploadArticle(request)); } + @Operation(summary = "게시글을 삭제하는 API 입니다.", description = """ + 게시글 id 를 path parameter 으로 넘겨주세요. + 해당 게시글을 삭제 상태로 변경합니다. + """) + @PutMapping("{article-id}/delete") + public ResponseEntity deleteArticle(@PathParam("article-id") Long articleId) { + return ResponseEntity.ok(articleService.deleteArticle(articleId)); + } + + @Operation(summary = "게시글 상세 조회 API 입니다.", description = """ + 게시글 id 를 path parameter 으로 넘겨주세요. + """) + @GetMapping("{article-id}") + public ResponseEntity getArticleDetails(@PathParam("article-id") Long articleId) { + return ResponseEntity.ok(articleService.getArticleDetail(articleId)); + } + + } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/DeleteArticleResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/DeleteArticleResponse.java new file mode 100644 index 00000000..ae7e249a --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/DeleteArticleResponse.java @@ -0,0 +1,24 @@ +package com.numberone.backend.domain.article.dto.response; + +import com.numberone.backend.domain.article.entity.Article; +import lombok.*; + +import java.time.LocalDateTime; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class DeleteArticleResponse { + + private Long id; + private LocalDateTime deletedAt; + + public static DeleteArticleResponse of(Article article){ + return DeleteArticleResponse.builder() + .id(article.getId()) + .deletedAt(article.getModifiedAt()) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java new file mode 100644 index 00000000..cb4e9676 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java @@ -0,0 +1,48 @@ +package com.numberone.backend.domain.article.dto.response; + +import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.member.entity.Member; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class GetArticleDetailResponse { + + // 게시글 관련 + private Long articleId; + private Integer likeCount; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + + // 작성자 관련 + private String memberName; + private String memberNickName; + private String address; // todo: 더미 데이터 + private Long ownerMemberId; + + // 이미지 관련 + private List imageUrls; + private String thumbNailImageUrl; + + public static GetArticleDetailResponse of(Article article, List imageUrls, String thumbNailImageUrl, Member member){ + return GetArticleDetailResponse.builder() + .articleId(article.getId()) + .likeCount(article.getLikeCount()) + .createdAt(article.getCreatedAt()) + .modifiedAt(article.getModifiedAt()) + .ownerMemberId(member.getId()) + .memberName(member.getRealName()) + .memberNickName(member.getNickName()) + .imageUrls(imageUrls) + .thumbNailImageUrl(thumbNailImageUrl) + .address("서울시 광진구 자양동") // 교체 + .build(); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index b26242cf..b422bf7d 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -72,4 +72,8 @@ public void updateArticleImage(List images, Long thumbNailImageUrl this.thumbNailImageUrlId = thumbNailImageUrlId; } + public void updateArticleStatus(ArticleStatus status){ + this.articleStatus = status; + } + } diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index 316cf53c..f73cd131 100644 --- a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -1,8 +1,11 @@ package com.numberone.backend.domain.article.service; import com.numberone.backend.domain.article.dto.request.UploadArticleRequest; +import com.numberone.backend.domain.article.dto.response.DeleteArticleResponse; +import com.numberone.backend.domain.article.dto.response.GetArticleDetailResponse; import com.numberone.backend.domain.article.dto.response.UploadArticleResponse; import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.article.entity.ArticleStatus; import com.numberone.backend.domain.article.repository.ArticleRepository; import com.numberone.backend.domain.articleimage.entity.ArticleImage; import com.numberone.backend.domain.articleimage.repository.ArticleImageRepository; @@ -11,6 +14,7 @@ import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.token.util.SecurityContextProvider; +import com.numberone.backend.exception.notfound.NotFoundArticleException; import com.numberone.backend.exception.notfound.NotFoundMemberException; import com.numberone.backend.support.s3.S3Provider; import lombok.RequiredArgsConstructor; @@ -22,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; @Slf4j @RequiredArgsConstructor @@ -83,4 +88,36 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { return UploadArticleResponse.of(article, imageUrls, thumbNailImageUrl); } + + @Transactional + public DeleteArticleResponse deleteArticle(Long articleId) { + Article article = articleRepository.findById(articleId) + .orElseThrow(NotFoundArticleException::new); + article.updateArticleStatus(ArticleStatus.DELETED); + return DeleteArticleResponse.of(article); + } + + public GetArticleDetailResponse getArticleDetail(Long articleId) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member owner = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + Article article = articleRepository.findById(articleId) + .orElseThrow(NotFoundArticleException::new); + + List imageUrls = articleImageRepository.findByArticle(article) + .stream() + .map(ArticleImage::getImageUrl) + .toList(); + + + Optional thumbNailImage = articleImageRepository.findById(article.getThumbNailImageUrlId()); + + String thumbNailImageUrl = ""; + if (thumbNailImage.isPresent()) { + thumbNailImageUrl = thumbNailImage.get().getImageUrl(); + } + + + return GetArticleDetailResponse.of(article, imageUrls, thumbNailImageUrl, owner); + } } diff --git a/src/main/java/com/numberone/backend/domain/articleimage/repository/ArticleImageRepository.java b/src/main/java/com/numberone/backend/domain/articleimage/repository/ArticleImageRepository.java index 5a835d7f..08bf579b 100644 --- a/src/main/java/com/numberone/backend/domain/articleimage/repository/ArticleImageRepository.java +++ b/src/main/java/com/numberone/backend/domain/articleimage/repository/ArticleImageRepository.java @@ -1,7 +1,12 @@ package com.numberone.backend.domain.articleimage.repository; +import com.numberone.backend.domain.article.entity.Article; import com.numberone.backend.domain.articleimage.entity.ArticleImage; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface ArticleImageRepository extends JpaRepository { + + List findByArticle(Article article); } diff --git a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java index 678519b3..e7b1e330 100644 --- a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java +++ b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java @@ -36,7 +36,13 @@ public enum CustomExceptionContext implements ExceptionContext { //후원 페이지 관련 예외 NOT_FOUND_SUPPORT("존재하지 않는 후원 관계입니다.", 7000), NOT_FOUND_SPONSOR("존재하지 않는 후원입니다.", 7001), - BAD_REQUEST_HEART("후원을 하기에는 사용자의 마음 갯수가 부족합니다.",7002) + BAD_REQUEST_HEART("후원을 하기에는 사용자의 마음 갯수가 부족합니다.",7002), + + // article 관련 예외 + NOT_FOUND_ARTICLE("해당 게시글을 찾을 수 없습니다.", 8000), + + // article image 관련 예외 + NOT_FOUND_ARTICLE_IMAGE("해당 이미지를 찾을 수 없습니다.", 9000), ; private final String message; diff --git a/src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleException.java b/src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleException.java new file mode 100644 index 00000000..47ac9824 --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleException.java @@ -0,0 +1,9 @@ +package com.numberone.backend.exception.notfound; + +import static com.numberone.backend.exception.context.CustomExceptionContext.NOT_FOUND_ARTICLE; + +public class NotFoundArticleException extends NotFoundException { + public NotFoundArticleException() { + super(NOT_FOUND_ARTICLE); + } +} diff --git a/src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleImageException.java b/src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleImageException.java new file mode 100644 index 00000000..cb031b3b --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleImageException.java @@ -0,0 +1,9 @@ +package com.numberone.backend.exception.notfound; + +import static com.numberone.backend.exception.context.CustomExceptionContext.NOT_FOUND_ARTICLE_IMAGE; + +public class NotFoundArticleImageException extends NotFoundException { + public NotFoundArticleImageException() { + super(NOT_FOUND_ARTICLE_IMAGE); + } +} From 5f57b33fb92c639d13ac60fbf36e223b80fd7fe7 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 20:53:16 +0900 Subject: [PATCH 15/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C,=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=20=EB=B0=8F=20=EB=82=B4=EC=9A=A9=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/controller/ArticleController.java | 8 ++++---- .../dto/response/GetArticleDetailResponse.java | 11 ++++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index d88995bf..8aa3deb5 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -46,19 +46,19 @@ public ResponseEntity uploadArticle(@RequestBody @Valid U } @Operation(summary = "게시글을 삭제하는 API 입니다.", description = """ - 게시글 id 를 path parameter 으로 넘겨주세요. + 게시글 id 를 PathVariable 으로 넘겨주세요. 해당 게시글을 삭제 상태로 변경합니다. """) @PutMapping("{article-id}/delete") - public ResponseEntity deleteArticle(@PathParam("article-id") Long articleId) { + public ResponseEntity deleteArticle(@PathVariable("article-id") Long articleId) { return ResponseEntity.ok(articleService.deleteArticle(articleId)); } @Operation(summary = "게시글 상세 조회 API 입니다.", description = """ - 게시글 id 를 path parameter 으로 넘겨주세요. + 게시글 id 를 PathVariable 으로 넘겨주세요. """) @GetMapping("{article-id}") - public ResponseEntity getArticleDetails(@PathParam("article-id") Long articleId) { + public ResponseEntity getArticleDetails(@PathVariable("article-id") Long articleId) { return ResponseEntity.ok(articleService.getArticleDetail(articleId)); } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java index cb4e9676..cc356667 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; @ToString @Builder @@ -19,6 +20,8 @@ public class GetArticleDetailResponse { private Integer likeCount; private LocalDateTime createdAt; private LocalDateTime modifiedAt; + private String title; + private String content; // 작성자 관련 private String memberName; @@ -33,7 +36,13 @@ public class GetArticleDetailResponse { public static GetArticleDetailResponse of(Article article, List imageUrls, String thumbNailImageUrl, Member member){ return GetArticleDetailResponse.builder() .articleId(article.getId()) - .likeCount(article.getLikeCount()) + .title(article.getTitle()) + .content(article.getContent()) + .likeCount( + Optional.ofNullable( + article.getLikeCount() + ).orElse(0) + ) .createdAt(article.getCreatedAt()) .modifiedAt(article.getModifiedAt()) .ownerMemberId(member.getId()) From cdb35efde28646d61c43d95b9f961d335b20105c Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 21:07:45 +0900 Subject: [PATCH 16/37] Feat(#54): removed test api for fcm --- .../controller/NotificationController.java | 31 ------------------- .../notification/dto/SendFcmRequest.java | 19 ------------ .../notification/dto/SendFcmResponse.java | 12 ------- .../service/NotificationService.java | 16 ---------- 4 files changed, 78 deletions(-) delete mode 100644 src/main/java/com/numberone/backend/domain/notification/controller/NotificationController.java delete mode 100644 src/main/java/com/numberone/backend/domain/notification/dto/SendFcmRequest.java delete mode 100644 src/main/java/com/numberone/backend/domain/notification/dto/SendFcmResponse.java diff --git a/src/main/java/com/numberone/backend/domain/notification/controller/NotificationController.java b/src/main/java/com/numberone/backend/domain/notification/controller/NotificationController.java deleted file mode 100644 index 0078b16c..00000000 --- a/src/main/java/com/numberone/backend/domain/notification/controller/NotificationController.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.numberone.backend.domain.notification.controller; - -import com.numberone.backend.domain.notification.dto.SendFcmRequest; -import com.numberone.backend.domain.notification.dto.SendFcmResponse; -import com.numberone.backend.domain.notification.service.NotificationService; -import io.swagger.v3.oas.annotations.Operation; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Slf4j -@RequestMapping("/notification") -@RestController -@RequiredArgsConstructor -public class NotificationController { - private final NotificationService notificationService; - - @Operation(summary = "fcm 푸시알람 테스트 용 API 입니다.", - description = " 테스트 해본 뒤, 성공 여부를 알려주세요. 🥲") - @PostMapping("/send-fcm") - public ResponseEntity sendFcmNotification(@RequestBody SendFcmRequest request) { - /* FCM 푸시알람 API 테스트 용 서비스 로직입니다. */ - notificationService.sendFcm(request); - return ResponseEntity.ok(new SendFcmResponse("메세지 전송 완료, 성공 여부를 백엔드 팀에게 알려주세요.")); - } - -} diff --git a/src/main/java/com/numberone/backend/domain/notification/dto/SendFcmRequest.java b/src/main/java/com/numberone/backend/domain/notification/dto/SendFcmRequest.java deleted file mode 100644 index 9dfbf107..00000000 --- a/src/main/java/com/numberone/backend/domain/notification/dto/SendFcmRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.numberone.backend.domain.notification.dto; - -import jakarta.validation.constraints.NotNull; -import lombok.*; - -@ToString -@Builder -@Getter -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class SendFcmRequest { - @NotNull(message = "fcmToken 은 null 일 수 없습니다.") - private String fcmToken; - - private String body; - private String title; - private String imageUrl; - -} diff --git a/src/main/java/com/numberone/backend/domain/notification/dto/SendFcmResponse.java b/src/main/java/com/numberone/backend/domain/notification/dto/SendFcmResponse.java deleted file mode 100644 index fc640142..00000000 --- a/src/main/java/com/numberone/backend/domain/notification/dto/SendFcmResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.numberone.backend.domain.notification.dto; - -import lombok.*; - -@ToString -@Builder -@Getter -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class SendFcmResponse { - private String msg; -} diff --git a/src/main/java/com/numberone/backend/domain/notification/service/NotificationService.java b/src/main/java/com/numberone/backend/domain/notification/service/NotificationService.java index e052044b..e8bcaf7a 100644 --- a/src/main/java/com/numberone/backend/domain/notification/service/NotificationService.java +++ b/src/main/java/com/numberone/backend/domain/notification/service/NotificationService.java @@ -1,30 +1,14 @@ package com.numberone.backend.domain.notification.service; -import com.numberone.backend.domain.notification.dto.SendFcmRequest; -import com.numberone.backend.support.fcm.dto.FcmNotificationDto; -import com.numberone.backend.support.fcm.service.FcmMessageProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Arrays; -import java.util.List; - @Slf4j @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class NotificationService { - private final FcmMessageProvider fcmMessageProvider; - public void sendFcm(SendFcmRequest request) { - /* FCM 푸시알람 API 테스트 용 서비스 로직입니다. */ - String token = request.getFcmToken(); - List tokens = Arrays.asList(request.getFcmToken()); - fcmMessageProvider.sendFcmToMembers( - tokens, - FcmNotificationDto.of(request.getTitle(), request.getBody(), request.getImageUrl()) - ); - } } From 74fc80071598f423d6b6ca93c123629a50e9f6be Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Sun, 12 Nov 2023 22:05:26 +0900 Subject: [PATCH 17/37] Feat(#54): removed test api for fcm --- .../notification/service/NotificationService.java | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 src/main/java/com/numberone/backend/domain/notification/service/NotificationService.java diff --git a/src/main/java/com/numberone/backend/domain/notification/service/NotificationService.java b/src/main/java/com/numberone/backend/domain/notification/service/NotificationService.java deleted file mode 100644 index e8bcaf7a..00000000 --- a/src/main/java/com/numberone/backend/domain/notification/service/NotificationService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.numberone.backend.domain.notification.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -public class NotificationService { - -} From bf09ccfca97b3b17acc0279e49106423a602ea8e Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Mon, 13 Nov 2023 01:24:41 +0900 Subject: [PATCH 18/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20no=20offset=20paging=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 31 +++++++- .../dto/response/ArticleSearchParameter.java | 14 ++++ .../dto/response/GetArticleListResponse.java | 72 +++++++++++++++++++ .../domain/article/entity/Article.java | 6 ++ .../article/repository/ArticleRepository.java | 3 +- .../custom/ArticleRepositoryCustom.java | 10 +++ .../custom/ArticleRepositoryCustomImpl.java | 69 ++++++++++++++++++ .../article/service/ArticleService.java | 29 ++++++-- 8 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java create mode 100644 src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java create mode 100644 src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustom.java create mode 100644 src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index 8aa3deb5..1b55b091 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -1,15 +1,15 @@ package com.numberone.backend.domain.article.controller; import com.numberone.backend.domain.article.dto.request.UploadArticleRequest; -import com.numberone.backend.domain.article.dto.response.DeleteArticleResponse; -import com.numberone.backend.domain.article.dto.response.GetArticleDetailResponse; -import com.numberone.backend.domain.article.dto.response.UploadArticleResponse; +import com.numberone.backend.domain.article.dto.response.*; import com.numberone.backend.domain.article.service.ArticleService; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import jakarta.websocket.server.PathParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -63,4 +63,29 @@ public ResponseEntity getArticleDetails(@PathVariable( } + @Operation(summary = "게시글 리스트 조회 no offset Paging API 입니다.", description = """ + + 요청 예시 url 은 다음과 같습니다. + `/api/articles?size=5` + size 는 페이지의 사이즈를 의미하고, default 는 20 입니다. + + 정렬 순서는 articleId 순입니다. ( = 생성 시간 순 ) + + ModelAttribute 로 lastArticleId 와 tag 를 넘겨주세요 ( 둘 다 nullable ) + + tag 가 null 이면, tag 상관 없이 전체 조회를 수행합니다. + tag 가 null 이 아니면, 해당 tag 에 해당하는 게시글만 조회합니다. + + lastArticleId 는 직전에 조회한 게시글 중 가장 먀지막(작은) articleId 를 의미합니다. + - 첫 페이지를 요청할 경우에는 lastArticleId 를 null 로 보내야합니다. + - 첫 페이지 이후에 대한 요청은, 직전 페이지 요청에서 얻어온 lastArticleId 를 넣어서 보내면 그 다음 페이지를 호출합니다. + + """) + @GetMapping + public ResponseEntity> getArticlePages( + Pageable pageable, + @ModelAttribute ArticleSearchParameter param){ + return ResponseEntity.ok(articleService.getArticleListPaging(param, pageable)); + } + } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java b/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java new file mode 100644 index 00000000..18da68f9 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java @@ -0,0 +1,14 @@ +package com.numberone.backend.domain.article.dto.response; + +import com.numberone.backend.domain.article.entity.ArticleTag; +import lombok.*; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ArticleSearchParameter { + private ArticleTag tag; + private Long lastArticleId; +} diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java new file mode 100644 index 00000000..6b88af6d --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java @@ -0,0 +1,72 @@ +package com.numberone.backend.domain.article.dto.response; + +import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.article.entity.ArticleStatus; +import com.numberone.backend.domain.article.entity.ArticleTag; +import com.numberone.backend.domain.articleimage.entity.ArticleImage; +import com.numberone.backend.domain.member.entity.Member; +import com.querydsl.core.annotations.QueryProjection; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.Optional; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class GetArticleListResponse { + + private ArticleTag tag; + private Long id; + private String title; + private String content; + private String address; + private String ownerNickName; + private Long ownerId; + private LocalDateTime createdAt; + private ArticleStatus articleStatus; + private String thumbNailImageUrl; + private Long thumbNailImageId; + + private Integer articleLikeCount; + private Integer commentCount; + + + @QueryProjection + public GetArticleListResponse(Article article, Long ownerId, Long thumbNailImageId) { + this.tag = article.getArticleTag(); + this.id = article.getId(); + this.title = article.getTitle(); + this.content = article.getContent(); + this.address = article.getAddress(); + this.ownerId = ownerId; + this.createdAt = article.getCreatedAt(); + this.articleStatus = article.getArticleStatus(); + this.thumbNailImageId = thumbNailImageId; + this.articleLikeCount = article.getLikeCount(); + this.commentCount = article.getCommentCount(); + } + + public void setOwnerNickName(String nickName){ + this.ownerNickName = nickName; + } + + public void setThumbNailImageUrl(String thumbNailImageUrl){ + this.thumbNailImageUrl = thumbNailImageUrl; + } + + public void updateInfo(Optional owner, Optional articleImage){ + owner.ifPresentOrElse( + o -> setOwnerNickName(o.getNickName()), + () -> setOwnerNickName("알 수 없는 사용자") + ); + articleImage.ifPresentOrElse( + image -> setThumbNailImageUrl(image.getImageUrl()), + () -> setThumbNailImageUrl("") + ); + } + + +} diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index b422bf7d..5e082b5e 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -8,6 +8,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.Comment; import java.util.ArrayList; @@ -53,9 +54,14 @@ public class Article extends BaseTimeEntity { @Comment("게시글 작성 당시 주소") private String address; + @ColumnDefault("0") @Comment("게시글 좋아요 개수") private Integer likeCount; // todo: 동시성 처리 + @ColumnDefault("0") + @Comment("게시글에 달린 댓글 개수") + private Integer commentCount; + @Comment("작성자 ID") private Long articleOwnerId; diff --git a/src/main/java/com/numberone/backend/domain/article/repository/ArticleRepository.java b/src/main/java/com/numberone/backend/domain/article/repository/ArticleRepository.java index 3f2d5489..b0a9d891 100644 --- a/src/main/java/com/numberone/backend/domain/article/repository/ArticleRepository.java +++ b/src/main/java/com/numberone/backend/domain/article/repository/ArticleRepository.java @@ -1,7 +1,8 @@ package com.numberone.backend.domain.article.repository; import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.article.repository.custom.ArticleRepositoryCustom; import org.springframework.data.jpa.repository.JpaRepository; -public interface ArticleRepository extends JpaRepository { +public interface ArticleRepository extends JpaRepository, ArticleRepositoryCustom { } diff --git a/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustom.java b/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustom.java new file mode 100644 index 00000000..7b65f173 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.numberone.backend.domain.article.repository.custom; + +import com.numberone.backend.domain.article.dto.response.ArticleSearchParameter; +import com.numberone.backend.domain.article.dto.response.GetArticleListResponse; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface ArticleRepositoryCustom { + Slice getArticlesNoOffSetPaging(ArticleSearchParameter param, Pageable pageable); +} diff --git a/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java b/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java new file mode 100644 index 00000000..f134e3e7 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java @@ -0,0 +1,69 @@ +package com.numberone.backend.domain.article.repository.custom; + +import com.numberone.backend.domain.article.dto.response.ArticleSearchParameter; +import com.numberone.backend.domain.article.dto.response.GetArticleListResponse; +import com.numberone.backend.domain.article.dto.response.QGetArticleListResponse; +import com.numberone.backend.domain.article.entity.ArticleTag; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import java.util.List; +import java.util.Objects; + +import static com.numberone.backend.domain.article.entity.QArticle.article; + +public class ArticleRepositoryCustomImpl implements ArticleRepositoryCustom { + private final JPAQueryFactory queryFactory; + + public ArticleRepositoryCustomImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Slice getArticlesNoOffSetPaging(ArticleSearchParameter param, Pageable pageable) { + List result = queryFactory.select(new QGetArticleListResponse( + article, + article.articleOwnerId, + article.thumbNailImageUrlId + )) + .from(article) + .where( + ltArticleId(param.getLastArticleId()), + checkTagCondition(param.getTag()) + ) + .orderBy(article.id.desc()) + .limit(pageable.getPageSize() + 1) + .fetch(); + return checkLastPage(pageable, result); + } + + private BooleanExpression checkTagCondition(ArticleTag tag) { + if (Objects.isNull(tag)) { + return null; + } + return article.articleTag.eq(tag); + } + + private BooleanExpression ltArticleId(Long articleId) { + if (Objects.isNull(articleId)) { + return null; + } + return article.id.lt(articleId); + } + + private Slice checkLastPage(Pageable pageable, List result) { + boolean hasNext = false; + + if (result.size() > pageable.getPageSize()) { + hasNext = true; + result.remove(pageable.getPageSize()); + } + + return new SliceImpl<>(result, pageable, hasNext); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index f73cd131..b354f42c 100644 --- a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -1,9 +1,7 @@ package com.numberone.backend.domain.article.service; import com.numberone.backend.domain.article.dto.request.UploadArticleRequest; -import com.numberone.backend.domain.article.dto.response.DeleteArticleResponse; -import com.numberone.backend.domain.article.dto.response.GetArticleDetailResponse; -import com.numberone.backend.domain.article.dto.response.UploadArticleResponse; +import com.numberone.backend.domain.article.dto.response.*; import com.numberone.backend.domain.article.entity.Article; import com.numberone.backend.domain.article.entity.ArticleStatus; import com.numberone.backend.domain.article.repository.ArticleRepository; @@ -15,10 +13,14 @@ import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.token.util.SecurityContextProvider; import com.numberone.backend.exception.notfound.NotFoundArticleException; +import com.numberone.backend.exception.notfound.NotFoundArticleImageException; import com.numberone.backend.exception.notfound.NotFoundMemberException; import com.numberone.backend.support.s3.S3Provider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -117,7 +119,26 @@ public GetArticleDetailResponse getArticleDetail(Long articleId) { thumbNailImageUrl = thumbNailImage.get().getImageUrl(); } - return GetArticleDetailResponse.of(article, imageUrls, thumbNailImageUrl, owner); } + + public Slice getArticleListPaging(ArticleSearchParameter param, Pageable pageable) { + return new SliceImpl<>( + articleRepository.getArticlesNoOffSetPaging(param, pageable) + .stream() + .peek(this::updateArticleInfo) + .toList() + ); + } + + public void updateArticleInfo(GetArticleListResponse articleInfo) { + Long ownerId = articleInfo.getOwnerId(); + Long thumbNailImageUrlId = articleInfo.getThumbNailImageId(); + + Optional owner = memberRepository.findById(ownerId); + Optional articleImage = articleImageRepository.findById(thumbNailImageUrlId); + + articleInfo.updateInfo(owner, articleImage); + } + } From dd67758d0a2dbe960b71aaee63ca28272dfd4801 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Mon, 13 Nov 2023 22:21:30 +0900 Subject: [PATCH 19/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=EC=97=90=20=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 57 ++++++++++++------- .../article/service/ArticleService.java | 23 +++++++- .../entity/ArticleParticipant.java | 5 +- .../dto/request/CreateCommentRequest.java | 17 ++++++ .../dto/response/CreateCommentResponse.java | 23 ++++++++ .../domain/comment/entity/CommentEntity.java | 10 ++++ 6 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/comment/dto/request/CreateCommentRequest.java create mode 100644 src/main/java/com/numberone/backend/domain/comment/dto/response/CreateCommentResponse.java diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index 1b55b091..c6ae0ad0 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -3,9 +3,10 @@ import com.numberone.backend.domain.article.dto.request.UploadArticleRequest; import com.numberone.backend.domain.article.dto.response.*; import com.numberone.backend.domain.article.service.ArticleService; +import com.numberone.backend.domain.comment.dto.request.CreateCommentRequest; +import com.numberone.backend.domain.comment.dto.response.CreateCommentResponse; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; -import jakarta.websocket.server.PathParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; @@ -64,28 +65,46 @@ public ResponseEntity getArticleDetails(@PathVariable( @Operation(summary = "게시글 리스트 조회 no offset Paging API 입니다.", description = """ - - 요청 예시 url 은 다음과 같습니다. - `/api/articles?size=5` - size 는 페이지의 사이즈를 의미하고, default 는 20 입니다. - - 정렬 순서는 articleId 순입니다. ( = 생성 시간 순 ) - - ModelAttribute 로 lastArticleId 와 tag 를 넘겨주세요 ( 둘 다 nullable ) - - tag 가 null 이면, tag 상관 없이 전체 조회를 수행합니다. - tag 가 null 이 아니면, 해당 tag 에 해당하는 게시글만 조회합니다. - - lastArticleId 는 직전에 조회한 게시글 중 가장 먀지막(작은) articleId 를 의미합니다. - - 첫 페이지를 요청할 경우에는 lastArticleId 를 null 로 보내야합니다. - - 첫 페이지 이후에 대한 요청은, 직전 페이지 요청에서 얻어온 lastArticleId 를 넣어서 보내면 그 다음 페이지를 호출합니다. - - """) + + 요청 예시 url 은 다음과 같습니다. + `/api/articles?size=5` + size 는 페이지의 사이즈를 의미하고, default 는 20 입니다. + + 정렬 순서는 articleId 순입니다. ( = 생성 시간 순 ) + + ModelAttribute 로 lastArticleId 와 tag 를 넘겨주세요 ( 둘 다 nullable ) + + tag 가 null 이면, tag 상관 없이 전체 조회를 수행합니다. + tag 가 null 이 아니면, 해당 tag 에 해당하는 게시글만 조회합니다. + + lastArticleId 는 직전에 조회한 게시글 중 가장 먀지막(작은) articleId 를 의미합니다. + - 첫 페이지를 요청할 경우에는 lastArticleId 를 null 로 보내야합니다. + - 첫 페이지 이후에 대한 요청은, 직전 페이지 요청에서 얻어온 lastArticleId 를 넣어서 보내면 그 다음 페이지를 호출합니다. + + """) @GetMapping public ResponseEntity> getArticlePages( Pageable pageable, - @ModelAttribute ArticleSearchParameter param){ + @ModelAttribute ArticleSearchParameter param) { return ResponseEntity.ok(articleService.getArticleListPaging(param, pageable)); } + @Operation(summary = "게시글에 댓글 작성하기", description = """ + 게시글에 댓글을 작성하는 API 입니다. + + 게시글 아이디는 Path Parameter 으로 넘겨주세요 (article-id) + + """) + @PostMapping("comments/{article-id}") + public ResponseEntity createComment( + @PathVariable("article-id") Long articleId, + @RequestBody @Valid CreateCommentRequest request) { + return ResponseEntity.created( + URI.create(String.format("/api/articles/comment/%s", articleId))) + .body(articleService.createComment(articleId, request)); + + } + + + } diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index b354f42c..3992981c 100644 --- a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -9,6 +9,10 @@ import com.numberone.backend.domain.articleimage.repository.ArticleImageRepository; import com.numberone.backend.domain.articleparticipant.entity.ArticleParticipant; import com.numberone.backend.domain.articleparticipant.repository.ArticleParticipantRepository; +import com.numberone.backend.domain.comment.dto.request.CreateCommentRequest; +import com.numberone.backend.domain.comment.dto.response.CreateCommentResponse; +import com.numberone.backend.domain.comment.entity.CommentEntity; +import com.numberone.backend.domain.comment.repository.CommentRepository; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.token.util.SecurityContextProvider; @@ -40,6 +44,7 @@ public class ArticleService { private final MemberRepository memberRepository; private final ArticleParticipantRepository articleParticipantRepository; private final ArticleImageRepository articleImageRepository; + private final CommentRepository commentRepository; private final S3Provider s3Provider; @Transactional @@ -57,7 +62,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { request.getArticleTag()) ); articleParticipantRepository.save( - new ArticleParticipant(article, owner.getId()) + new ArticleParticipant(article, owner) ); // 2. 이미지 업로드 @@ -141,4 +146,20 @@ public void updateArticleInfo(GetArticleListResponse articleInfo) { articleInfo.updateInfo(owner, articleImage); } + @Transactional + public CreateCommentResponse createComment(Long articleId, CreateCommentRequest request){ + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + Article article = articleRepository.findById(articleId) + .orElseThrow(NotFoundArticleException::new); + CommentEntity savedComment = commentRepository.save( + new CommentEntity(request.getContent(), article) + ); + + articleParticipantRepository.save(new ArticleParticipant(article, member)); + + return CreateCommentResponse.of(savedComment); + } + } diff --git a/src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java b/src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java index 92423f2a..0e3e9452 100644 --- a/src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java +++ b/src/main/java/com/numberone/backend/domain/articleparticipant/entity/ArticleParticipant.java @@ -2,6 +2,7 @@ import com.numberone.backend.config.basetime.BaseTimeEntity; import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.member.entity.Member; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -28,8 +29,8 @@ public class ArticleParticipant extends BaseTimeEntity { @Comment("회원 id") private Long memberId; - public ArticleParticipant(Article article, Long memberId){ + public ArticleParticipant(Article article, Member member){ this.article = article; - this.memberId= memberId; + this.memberId= member.getId(); } } diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/request/CreateCommentRequest.java b/src/main/java/com/numberone/backend/domain/comment/dto/request/CreateCommentRequest.java new file mode 100644 index 00000000..01925dc2 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/dto/request/CreateCommentRequest.java @@ -0,0 +1,17 @@ +package com.numberone.backend.domain.comment.dto.request; + +import com.numberone.backend.config.basetime.BaseTimeEntity; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class CreateCommentRequest { + + @NotNull + private String content; + +} diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/response/CreateCommentResponse.java b/src/main/java/com/numberone/backend/domain/comment/dto/response/CreateCommentResponse.java new file mode 100644 index 00000000..d6c72e13 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/dto/response/CreateCommentResponse.java @@ -0,0 +1,23 @@ +package com.numberone.backend.domain.comment.dto.response; + +import com.numberone.backend.domain.comment.entity.CommentEntity; +import lombok.*; + +import java.time.LocalDateTime; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class CreateCommentResponse { + private LocalDateTime createdAt; + private Long commentId; + + public static CreateCommentResponse of (CommentEntity comment){ + return CreateCommentResponse.builder() + .createdAt(comment.getCreatedAt()) + .commentId(comment.getId()) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java index ff922c1f..bb2b3891 100644 --- a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java +++ b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java @@ -28,4 +28,14 @@ public class CommentEntity extends BaseTimeEntity { @Comment("댓글 좋아요 개수") private Integer likeCount; // todo: 동시성 처리 + + @Comment("댓글 내용") + private String content; + + public CommentEntity(String content, Article article){ + this.depth = 0; + this.content = content; + this.article = article; + } + } From cfc149d2901ceba5dae6dd2cb6784e36462e6aa5 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Mon, 13 Nov 2023 23:19:50 +0900 Subject: [PATCH 20/37] =?UTF-8?q?Feat(#54):=20=EB=8C=80=EB=8C=93=EA=B8=80?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 38 +++++++++++++++ .../request/CreateChildCommentRequest.java | 15 ++++++ .../response/CreateChildCommentResponse.java | 25 ++++++++++ .../dto/response/CreateCommentResponse.java | 1 + .../domain/comment/entity/CommentEntity.java | 16 +++++++ .../comment/service/CommentService.java | 46 +++++++++++++++++++ .../context/CustomExceptionContext.java | 3 ++ .../notfound/NotFoundCommentException.java | 9 ++++ 8 files changed, 153 insertions(+) create mode 100644 src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java create mode 100644 src/main/java/com/numberone/backend/domain/comment/dto/request/CreateChildCommentRequest.java create mode 100644 src/main/java/com/numberone/backend/domain/comment/dto/response/CreateChildCommentResponse.java create mode 100644 src/main/java/com/numberone/backend/domain/comment/service/CommentService.java create mode 100644 src/main/java/com/numberone/backend/exception/notfound/NotFoundCommentException.java diff --git a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java new file mode 100644 index 00000000..dbd4ab38 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java @@ -0,0 +1,38 @@ +package com.numberone.backend.domain.comment.controller; + +import com.numberone.backend.domain.comment.dto.request.CreateChildCommentRequest; +import com.numberone.backend.domain.comment.dto.response.CreateChildCommentResponse; +import com.numberone.backend.domain.comment.service.CommentService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; + +@Slf4j +@RequestMapping("/comments") +@RequiredArgsConstructor +@RestController +public class CommentController { + + private final CommentService commentService; + + @Operation(summary = "대댓글 작성 API", description = """ + comment-id 는 부모 댓글의 id 입니다. + article-id 와 comment-id 는 모두 path variable 으로 보내주세요! + """) + @PostMapping("{article-id}/{comment-id}") + public ResponseEntity createChildComment( + @PathVariable("article-id") Long articleId, + @PathVariable("comment-id") Long commentId, + @RequestBody @Valid CreateChildCommentRequest request ){ + CreateChildCommentResponse response = commentService.createChildComment(articleId, commentId, request); + return ResponseEntity.created( + URI.create(String.format("/comments/%s/%s", articleId, commentId))) + .body(response); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/request/CreateChildCommentRequest.java b/src/main/java/com/numberone/backend/domain/comment/dto/request/CreateChildCommentRequest.java new file mode 100644 index 00000000..5b765e43 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/dto/request/CreateChildCommentRequest.java @@ -0,0 +1,15 @@ +package com.numberone.backend.domain.comment.dto.request; + +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class CreateChildCommentRequest { + @NotNull + private String content; + +} diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/response/CreateChildCommentResponse.java b/src/main/java/com/numberone/backend/domain/comment/dto/response/CreateChildCommentResponse.java new file mode 100644 index 00000000..80d65519 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/dto/response/CreateChildCommentResponse.java @@ -0,0 +1,25 @@ +package com.numberone.backend.domain.comment.dto.response; + +import com.numberone.backend.domain.comment.entity.CommentEntity; +import lombok.*; + +import java.time.LocalDateTime; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class CreateChildCommentResponse { + + private LocalDateTime createdAt; + private Long commentId; + + public static CreateChildCommentResponse of (CommentEntity comment){ + return CreateChildCommentResponse.builder() + .createdAt(comment.getCreatedAt()) + .commentId(comment.getId()) + .build(); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/response/CreateCommentResponse.java b/src/main/java/com/numberone/backend/domain/comment/dto/response/CreateCommentResponse.java index d6c72e13..d1cbf278 100644 --- a/src/main/java/com/numberone/backend/domain/comment/dto/response/CreateCommentResponse.java +++ b/src/main/java/com/numberone/backend/domain/comment/dto/response/CreateCommentResponse.java @@ -20,4 +20,5 @@ public static CreateCommentResponse of (CommentEntity comment){ .commentId(comment.getId()) .build(); } + } diff --git a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java index bb2b3891..20cd2ff9 100644 --- a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java +++ b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java @@ -4,10 +4,14 @@ import com.numberone.backend.domain.article.entity.Article; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; +import java.util.ArrayList; +import java.util.List; + @Comment("동네생활 댓글 정보") @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -32,10 +36,22 @@ public class CommentEntity extends BaseTimeEntity { @Comment("댓글 내용") private String content; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private CommentEntity parent; + + @Builder.Default + @OneToMany(mappedBy = "parent", orphanRemoval = true) + private List childs = new ArrayList<>(); + public CommentEntity(String content, Article article){ this.depth = 0; this.content = content; this.article = article; } + public void updateParent(CommentEntity parent){ + this.parent = parent; + } + } diff --git a/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java b/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java new file mode 100644 index 00000000..8040abf3 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java @@ -0,0 +1,46 @@ +package com.numberone.backend.domain.comment.service; + +import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.article.repository.ArticleRepository; +import com.numberone.backend.domain.articleparticipant.entity.ArticleParticipant; +import com.numberone.backend.domain.articleparticipant.repository.ArticleParticipantRepository; +import com.numberone.backend.domain.comment.dto.request.CreateChildCommentRequest; +import com.numberone.backend.domain.comment.dto.response.CreateChildCommentResponse; +import com.numberone.backend.domain.comment.entity.CommentEntity; +import com.numberone.backend.domain.comment.repository.CommentRepository; +import com.numberone.backend.exception.notfound.NotFoundArticleException; +import com.numberone.backend.exception.notfound.NotFoundCommentException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class CommentService { + + private final CommentRepository commentRepository; + private final ArticleRepository articleRepository; + private final ArticleParticipantRepository articleParticipantRepository; + + @Transactional + public CreateChildCommentResponse createChildComment( + Long articleId, + Long parentCommentId, + CreateChildCommentRequest request){ + + Article article = articleRepository.findById(articleId) + .orElseThrow(NotFoundArticleException::new); + CommentEntity parentComment = commentRepository.findById(parentCommentId) + .orElseThrow(NotFoundCommentException::new); + + CommentEntity childComment = commentRepository.save(new CommentEntity(request.getContent(), article)); + + childComment.updateParent(parentComment); + + return CreateChildCommentResponse.of(childComment); + } + +} diff --git a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java index e7b1e330..cf99bf30 100644 --- a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java +++ b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java @@ -43,6 +43,9 @@ public enum CustomExceptionContext implements ExceptionContext { // article image 관련 예외 NOT_FOUND_ARTICLE_IMAGE("해당 이미지를 찾을 수 없습니다.", 9000), + + // comment 관련 예외 + NOT_FOUND_COMMENT("해당 댓글을 찾을 수 없습니다.", 10000), ; private final String message; diff --git a/src/main/java/com/numberone/backend/exception/notfound/NotFoundCommentException.java b/src/main/java/com/numberone/backend/exception/notfound/NotFoundCommentException.java new file mode 100644 index 00000000..c5f0e8db --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/notfound/NotFoundCommentException.java @@ -0,0 +1,9 @@ +package com.numberone.backend.exception.notfound; + +import static com.numberone.backend.exception.context.CustomExceptionContext.NOT_FOUND_COMMENT; + +public class NotFoundCommentException extends NotFoundException { + public NotFoundCommentException(){ + super(NOT_FOUND_COMMENT); + } +} From 30027a313bf36363460ee81745b1f814703a14ab Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Tue, 14 Nov 2023 00:50:49 +0900 Subject: [PATCH 21/37] =?UTF-8?q?Feat(#54):=20=EB=8C=80=EB=8C=93=EA=B8=80?= =?UTF-8?q?=20=EA=B3=84=EC=B8=B5=ED=98=95=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/service/ArticleService.java | 2 +- .../comment/controller/CommentController.java | 31 +++++++++- .../comment/dto/response/GetCommentDto.java | 62 +++++++++++++++++++ .../domain/comment/entity/CommentEntity.java | 8 ++- .../comment/repository/CommentRepository.java | 3 +- .../custom/CommentRepositoryCustom.java | 10 +++ .../custom/CommentRepositoryCustomImpl.java | 34 ++++++++++ .../comment/service/CommentService.java | 48 +++++++++++++- 8 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java create mode 100644 src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java create mode 100644 src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index 3992981c..26b4be8e 100644 --- a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -154,7 +154,7 @@ public CreateCommentResponse createComment(Long articleId, CreateCommentRequest Article article = articleRepository.findById(articleId) .orElseThrow(NotFoundArticleException::new); CommentEntity savedComment = commentRepository.save( - new CommentEntity(request.getContent(), article) + new CommentEntity(request.getContent(), article, member) ); articleParticipantRepository.save(new ArticleParticipant(article, member)); diff --git a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java index dbd4ab38..8910e1cf 100644 --- a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java +++ b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java @@ -2,6 +2,7 @@ import com.numberone.backend.domain.comment.dto.request.CreateChildCommentRequest; import com.numberone.backend.domain.comment.dto.response.CreateChildCommentResponse; +import com.numberone.backend.domain.comment.dto.response.GetCommentDto; import com.numberone.backend.domain.comment.service.CommentService; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; @@ -11,9 +12,10 @@ import org.springframework.web.bind.annotation.*; import java.net.URI; +import java.util.List; @Slf4j -@RequestMapping("/comments") +@RequestMapping("api/comments") @RequiredArgsConstructor @RestController public class CommentController { @@ -28,11 +30,34 @@ public class CommentController { public ResponseEntity createChildComment( @PathVariable("article-id") Long articleId, @PathVariable("comment-id") Long commentId, - @RequestBody @Valid CreateChildCommentRequest request ){ + @RequestBody @Valid CreateChildCommentRequest request) { CreateChildCommentResponse response = commentService.createChildComment(articleId, commentId, request); return ResponseEntity.created( - URI.create(String.format("/comments/%s/%s", articleId, commentId))) + URI.create(String.format("/comments/%s/%s", articleId, commentId))) .body(response); } + @Operation(summary = "해당 게시물에 달린 댓긂을 모두 조회하는 API 입니다.", description = """ + 해당 게시물에 달린 댓글을 계층 형태로 조회합니다. + + - Long commentId : 댓글 아이디 + - Long parentCommentId : 부모 댓글의 야이디 (nullable) + - List childComments = new ArrayList<>() : 대댓글 리스트 + - Integer likeCount : 해당 댓글의 좋아요 개수 + - LocalDateTime createdAt : 해당 댓글의 생성 시각 + - LocalDateTime modifiedAt : 해당 댓글의 마지막 수정 시각 + - String content : 해당 댓글의 내용 + - Long authorId : 해당 댓글의 작성자 아이디 + - String authorNickName : 해당 댓글의 작성자 닉네임 + - String authorProfileImageUrl : 해당 댓글 작성자의 프로필 사진 url + + 댓글 작성자가 추후에 탈퇴하는 경우를 고려했는데, + authorNickName 이 "알수없는 사용자" 로 변경되어 내려갑니다..! + """) + @GetMapping("{article-id}") + public ResponseEntity> getCommentsByArticle(@PathVariable("article-id") Long articleId){ + List response = commentService.getCommentsByArticle(articleId); + return ResponseEntity.ok(response); + } + } diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java b/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java new file mode 100644 index 00000000..4fe99af3 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java @@ -0,0 +1,62 @@ +package com.numberone.backend.domain.comment.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.comment.entity.CommentEntity; +import com.numberone.backend.domain.member.entity.Member; +import com.querydsl.core.annotations.QueryProjection; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class GetCommentDto { + + private Long commentId; + private Long parentCommentId; + private List childComments = new ArrayList<>(); + private Integer likeCount; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss a", timezone = "Asia/Seoul") + private LocalDateTime createdAt; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss a", timezone = "Asia/Seoul") + private LocalDateTime modifiedAt; + private String content; + private Long authorId; + private String authorNickName; + private String authorProfileImageUrl; + + + @QueryProjection + public GetCommentDto(CommentEntity comment){ + if(!Objects.isNull(comment.getParent())){ + this.parentCommentId = comment.getParent().getId(); + } + this.commentId = comment.getId(); + this.createdAt = comment.getCreatedAt(); + this.modifiedAt = comment.getModifiedAt(); + this.content = comment.getContent(); + this.authorId = comment.getAuthorId(); + this.likeCount = comment.getLikeCount(); + } + + public void updateCommentInfo(Optional author){ + author.ifPresentOrElse( + a -> { + this.authorNickName = a.getNickName(); + this.authorProfileImageUrl = a.getProfileImageUrl(); + }, + () -> { + this.authorNickName = "알 수 없는 사용자"; + } + ); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java index 20cd2ff9..7961af6f 100644 --- a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java +++ b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java @@ -2,6 +2,7 @@ import com.numberone.backend.config.basetime.BaseTimeEntity; import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.member.entity.Member; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -36,18 +37,21 @@ public class CommentEntity extends BaseTimeEntity { @Comment("댓글 내용") private String content; + @Comment("작성자 아이디") + private Long authorId; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private CommentEntity parent; - @Builder.Default @OneToMany(mappedBy = "parent", orphanRemoval = true) private List childs = new ArrayList<>(); - public CommentEntity(String content, Article article){ + public CommentEntity(String content, Article article, Member author){ this.depth = 0; this.content = content; this.article = article; + this.authorId = author.getId(); } public void updateParent(CommentEntity parent){ diff --git a/src/main/java/com/numberone/backend/domain/comment/repository/CommentRepository.java b/src/main/java/com/numberone/backend/domain/comment/repository/CommentRepository.java index d86de378..6ffcf463 100644 --- a/src/main/java/com/numberone/backend/domain/comment/repository/CommentRepository.java +++ b/src/main/java/com/numberone/backend/domain/comment/repository/CommentRepository.java @@ -1,7 +1,8 @@ package com.numberone.backend.domain.comment.repository; import com.numberone.backend.domain.comment.entity.CommentEntity; +import com.numberone.backend.domain.comment.repository.custom.CommentRepositoryCustom; import org.springframework.data.jpa.repository.JpaRepository; -public interface CommentRepository extends JpaRepository { +public interface CommentRepository extends JpaRepository, CommentRepositoryCustom { } diff --git a/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java new file mode 100644 index 00000000..fe326c39 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.numberone.backend.domain.comment.repository.custom; + +import com.numberone.backend.domain.comment.dto.response.GetCommentDto; + +import java.util.List; + +public interface CommentRepositoryCustom { + public List findAllByArticle(Long articleId); + +} diff --git a/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java new file mode 100644 index 00000000..69331895 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java @@ -0,0 +1,34 @@ +package com.numberone.backend.domain.comment.repository.custom; + +import com.numberone.backend.domain.article.entity.QArticle; +import com.numberone.backend.domain.comment.dto.response.GetCommentDto; +import com.numberone.backend.domain.comment.dto.response.QGetCommentDto; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; + +import java.util.List; + +import static com.numberone.backend.domain.article.entity.QArticle.article; +import static com.numberone.backend.domain.comment.entity.QCommentEntity.commentEntity; + +public class CommentRepositoryCustomImpl implements CommentRepositoryCustom { + private final JPAQueryFactory queryFactory; + + public CommentRepositoryCustomImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public List findAllByArticle(Long articleId) { + return queryFactory.select(new QGetCommentDto(commentEntity)) + .from(commentEntity) + .leftJoin(commentEntity.parent) + .fetchJoin() + .where(commentEntity.article.id.eq(articleId)) + .orderBy( + commentEntity.parent.id.asc().nullsFirst(), + commentEntity.createdAt.asc() + ) + .fetch(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java b/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java index 8040abf3..ed8b83d7 100644 --- a/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java +++ b/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java @@ -6,15 +6,22 @@ import com.numberone.backend.domain.articleparticipant.repository.ArticleParticipantRepository; import com.numberone.backend.domain.comment.dto.request.CreateChildCommentRequest; import com.numberone.backend.domain.comment.dto.response.CreateChildCommentResponse; +import com.numberone.backend.domain.comment.dto.response.GetCommentDto; import com.numberone.backend.domain.comment.entity.CommentEntity; import com.numberone.backend.domain.comment.repository.CommentRepository; +import com.numberone.backend.domain.member.entity.Member; +import com.numberone.backend.domain.member.repository.MemberRepository; +import com.numberone.backend.domain.token.util.SecurityContextProvider; import com.numberone.backend.exception.notfound.NotFoundArticleException; import com.numberone.backend.exception.notfound.NotFoundCommentException; +import com.numberone.backend.exception.notfound.NotFoundMemberException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.*; + @Slf4j @Transactional(readOnly = true) @RequiredArgsConstructor @@ -24,23 +31,58 @@ public class CommentService { private final CommentRepository commentRepository; private final ArticleRepository articleRepository; private final ArticleParticipantRepository articleParticipantRepository; + private final MemberRepository memberRepository; @Transactional public CreateChildCommentResponse createChildComment( Long articleId, Long parentCommentId, - CreateChildCommentRequest request){ + CreateChildCommentRequest request) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); Article article = articleRepository.findById(articleId) .orElseThrow(NotFoundArticleException::new); CommentEntity parentComment = commentRepository.findById(parentCommentId) .orElseThrow(NotFoundCommentException::new); - CommentEntity childComment = commentRepository.save(new CommentEntity(request.getContent(), article)); - + CommentEntity childComment = commentRepository.save(new CommentEntity(request.getContent(), article, member)); childComment.updateParent(parentComment); + articleParticipantRepository.save(new ArticleParticipant(article, member)); + return CreateChildCommentResponse.of(childComment); } + public List getCommentsByArticle(Long articleId) { + Article article = articleRepository.findById(articleId) + .orElseThrow(NotFoundArticleException::new); + List comments = commentRepository.findAllByArticle(article.getId()); + + // 계층 구조로 변환 (추후 리팩토링 필요) + List result = new ArrayList<>(); + Map map = new HashMap<>(); + comments.forEach( + comment -> { + CommentEntity commentEntity = commentRepository.findById(comment.getCommentId()) + .orElseThrow(NotFoundCommentException::new); + Optional author = memberRepository.findById(commentEntity.getAuthorId()); + comment.updateCommentInfo(author); + + map.put(comment.getCommentId(), comment); + + if (comment.getParentCommentId() != null){ + GetCommentDto parentComment = map.get(comment.getParentCommentId()); + List childComments = parentComment.getChildComments(); + childComments.add(comment); + } else { + result.add(comment); + } + } + ); + + return result; + } + } From 46a2c03a75793322755e6d6cbc9db3120f6d9449 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Tue, 14 Nov 2023 08:37:47 +0900 Subject: [PATCH 22/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=ED=95=98=EA=B8=B0=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20http=20method=20=EB=A5=BC=20patch=20=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 19 ++++++++- .../dto/request/ModifyArticleRequest.java | 38 +++++++++++++++++ .../dto/request/UploadArticleRequest.java | 3 ++ .../dto/response/ModifyArticleResponse.java | 41 +++++++++++++++++++ .../domain/article/entity/Article.java | 8 +++- .../article/service/ArticleService.java | 41 +++++++++++++++++++ .../dto/request/CreateCommentRequest.java | 3 ++ 7 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/article/dto/request/ModifyArticleRequest.java create mode 100644 src/main/java/com/numberone/backend/domain/article/dto/response/ModifyArticleResponse.java diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index c6ae0ad0..847d367e 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -1,5 +1,6 @@ package com.numberone.backend.domain.article.controller; +import com.numberone.backend.domain.article.dto.request.ModifyArticleRequest; import com.numberone.backend.domain.article.dto.request.UploadArticleRequest; import com.numberone.backend.domain.article.dto.response.*; import com.numberone.backend.domain.article.service.ArticleService; @@ -50,7 +51,7 @@ public ResponseEntity uploadArticle(@RequestBody @Valid U 게시글 id 를 PathVariable 으로 넘겨주세요. 해당 게시글을 삭제 상태로 변경합니다. """) - @PutMapping("{article-id}/delete") + @PatchMapping("{article-id}/delete") public ResponseEntity deleteArticle(@PathVariable("article-id") Long articleId) { return ResponseEntity.ok(articleService.deleteArticle(articleId)); } @@ -92,7 +93,7 @@ public ResponseEntity> getArticlePages( @Operation(summary = "게시글에 댓글 작성하기", description = """ 게시글에 댓글을 작성하는 API 입니다. - 게시글 아이디는 Path Parameter 으로 넘겨주세요 (article-id) + 게시글 아이디는 Path variable 으로 넘겨주세요 (article-id) """) @PostMapping("comments/{article-id}") @@ -105,6 +106,20 @@ public ResponseEntity createComment( } + @Operation(summary = "게시글 수정하기", description = """ + 게시글 내용을 수정하는 API 입니다. + + 반드시 access token 을 헤더에 포함해서 요청해주세요. + + article-id 는 path variable 으로 넘겨주세요. + """) + @PutMapping("{article-id}/modify") + public ResponseEntity modifyArticle( + @PathVariable("article-id") Long articleId, + @RequestBody @Valid ModifyArticleRequest request ){ + return ResponseEntity.ok(articleService.modifyArticle(articleId, request)); + } + } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/request/ModifyArticleRequest.java b/src/main/java/com/numberone/backend/domain/article/dto/request/ModifyArticleRequest.java new file mode 100644 index 00000000..3348de84 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/dto/request/ModifyArticleRequest.java @@ -0,0 +1,38 @@ +package com.numberone.backend.domain.article.dto.request; + +import com.numberone.backend.domain.article.entity.ArticleTag; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ModifyArticleRequest { + + // 글 관련 + @NotNull(message = "글 제목은 null 일 수 없습니다.") + private String title; // 제목 + + @NotNull(message = "내용은 null 일 수 없습니다.") + private String content; // 내용 + + @NotNull(message = """ + 게시글의 태그를 하나 선택해주세요. + + LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보) + """) + private ArticleTag articleTag; // 게시글 태그 + + // 이미지 관련 + private List imageList; // 이미지 리스트 + private Long thumbNailImageIdx; // 썸네일 이미지의 순서 (0,1,2,...) + + private Double longitude; + private Double latitude; + +} diff --git a/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java b/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java index 02aa22c8..04b76643 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java @@ -32,4 +32,7 @@ public class UploadArticleRequest { private List imageList; // 이미지 리스트 private Long thumbNailImageIdx; // 썸네일 이미지의 순서 (0,1,2,...) + private Double longitude; + private Double latitude; + } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/ModifyArticleResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/ModifyArticleResponse.java new file mode 100644 index 00000000..88a57536 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/ModifyArticleResponse.java @@ -0,0 +1,41 @@ +package com.numberone.backend.domain.article.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.numberone.backend.domain.article.entity.Article; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ModifyArticleResponse { + + private Long articleId; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss a", timezone = "Asia/Seoul") + private LocalDateTime createdAt; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss a", timezone = "Asia/Seoul") + private LocalDateTime modifiedAt; + + // 이미지 관련 + private List imageUrls; + private String thumbNailImageUrl; + + // 작성자 주소 + private String address; // todo: 더미 데이터 + + public static ModifyArticleResponse of(Article article, List imageUrls, String thumbNailImageUrl){ + return ModifyArticleResponse.builder() + .articleId(article.getId()) + .createdAt(article.getCreatedAt()) + .modifiedAt(article.getModifiedAt()) + .imageUrls(imageUrls) + .thumbNailImageUrl(thumbNailImageUrl) + .address("서울시 광진구 자양동") + .build(); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index 5e082b5e..7aa906ca 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -78,8 +78,14 @@ public void updateArticleImage(List images, Long thumbNailImageUrl this.thumbNailImageUrlId = thumbNailImageUrlId; } - public void updateArticleStatus(ArticleStatus status){ + public void updateArticleStatus(ArticleStatus status) { this.articleStatus = status; } + public void modifyArticle(String title, String content, ArticleTag tag) { + this.title = title; + this.content = content; + this.articleTag = tag; + } + } diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index 26b4be8e..194517d9 100644 --- a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -1,5 +1,6 @@ package com.numberone.backend.domain.article.service; +import com.numberone.backend.domain.article.dto.request.ModifyArticleRequest; import com.numberone.backend.domain.article.dto.request.UploadArticleRequest; import com.numberone.backend.domain.article.dto.response.*; import com.numberone.backend.domain.article.entity.Article; @@ -71,6 +72,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { String thumbNailImageUrl = ""; Long thumbNailImageId = 1L; if (!Objects.isNull(request.getImageList())) { + // todo: refactoring List imageList = request.getImageList(); for (int i = 0; i < imageList.size(); i++) { @@ -128,6 +130,7 @@ public GetArticleDetailResponse getArticleDetail(Long articleId) { } public Slice getArticleListPaging(ArticleSearchParameter param, Pageable pageable) { + // todo: 게시글 상태 고려하여 조회하기 (삭제 여부) return new SliceImpl<>( articleRepository.getArticlesNoOffSetPaging(param, pageable) .stream() @@ -162,4 +165,42 @@ public CreateCommentResponse createComment(Long articleId, CreateCommentRequest return CreateCommentResponse.of(savedComment); } + @Transactional + public ModifyArticleResponse modifyArticle(Long articleId, ModifyArticleRequest request){ + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + Article article = articleRepository.findById(articleId) + .orElseThrow(NotFoundArticleException::new); + + article.modifyArticle(request.getTitle(), request.getContent(), request.getArticleTag()); + + + List articleImages = new ArrayList<>(); + List imageUrls = new ArrayList<>(); + String thumbNailImageUrl = ""; + Long thumbNailImageId = 1L; + if (!Objects.isNull(request.getImageList())) { + // todo: refactoring + List imageList = request.getImageList(); + + for (int i = 0; i < imageList.size(); i++) { + String imageUrl = s3Provider.uploadImage(imageList.get(i)); + imageUrls.add(imageUrl); + + ArticleImage savedArticleImage = articleImageRepository.save( + new ArticleImage(article, imageUrl) + ); + articleImages.add(savedArticleImage); + if (Objects.equals(i, request.getThumbNailImageIdx())) { + thumbNailImageUrl = imageUrl; + thumbNailImageId = savedArticleImage.getId(); + } + + } + article.updateArticleImage(articleImages, thumbNailImageId); + } + + return ModifyArticleResponse.of(article, imageUrls, thumbNailImageUrl); + } } diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/request/CreateCommentRequest.java b/src/main/java/com/numberone/backend/domain/comment/dto/request/CreateCommentRequest.java index 01925dc2..057e6a72 100644 --- a/src/main/java/com/numberone/backend/domain/comment/dto/request/CreateCommentRequest.java +++ b/src/main/java/com/numberone/backend/domain/comment/dto/request/CreateCommentRequest.java @@ -14,4 +14,7 @@ public class CreateCommentRequest { @NotNull private String content; + private Double longitude; + private Double latitude; + } From 4647260be3c20ae64f920787a070436ea771e4d8 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Tue, 14 Nov 2023 08:44:27 +0900 Subject: [PATCH 23/37] =?UTF-8?q?Docs(#54):=20todo=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/article/controller/ArticleController.java | 3 +-- .../backend/domain/comment/controller/CommentController.java | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index 847d367e..dffd1d5a 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -25,7 +25,6 @@ public class ArticleController { private final ArticleService articleService; - @Operation(summary = "게시글 작성 API", description = """ 동네생활 게시글 등록 api 입니다. @@ -120,6 +119,6 @@ public ResponseEntity modifyArticle( return ResponseEntity.ok(articleService.modifyArticle(articleId, request)); } - + // todo: 작성자 위경도 주소 변환 처리, 게시글 좋아요, 게시글에 댓글 달리면 작성자에게 푸시알람 전송, 게시글 신고 기능 } diff --git a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java index 8910e1cf..3e3701de 100644 --- a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java +++ b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java @@ -60,4 +60,6 @@ public ResponseEntity> getCommentsByArticle(@PathVariable("a return ResponseEntity.ok(response); } + // todo: 댓글 삭제, 댓글 좋아요, 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능 + } From cb2b1080d5f4668ad9c1d1df5cf2cae9dadcc5e7 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Tue, 14 Nov 2023 22:29:59 +0900 Subject: [PATCH 24/37] Feat(#54): catch exception --- .../backend/util/LocationProvider.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/numberone/backend/util/LocationProvider.java b/src/main/java/com/numberone/backend/util/LocationProvider.java index 806f412b..ba0c2efb 100644 --- a/src/main/java/com/numberone/backend/util/LocationProvider.java +++ b/src/main/java/com/numberone/backend/util/LocationProvider.java @@ -16,22 +16,26 @@ @Component @RequiredArgsConstructor @Slf4j -//위치(주소나 GPS) 관련 기능들 작성할 util함수 public class LocationProvider { private final KakaoProperties kakaoProperties; private final RestTemplate restTemplate; public String pos2address(double latitude, double longitude) { - HttpHeaders headers = new HttpHeaders(); - headers.add("Authorization", "KakaoAK " + kakaoProperties.getClient_id()); - URI uri = UriComponentsBuilder - .fromUriString(kakaoProperties.getMapApiUrl()) - .queryParam("x", longitude) - .queryParam("y", latitude) - .build() - .toUri(); - MapApiResponse mapApiResponse = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(null, headers), MapApiResponse.class) - .getBody(); - return mapApiResponse.getDocuments().get(0).getAddress(); + try { + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "KakaoAK " + kakaoProperties.getClient_id()); + URI uri = UriComponentsBuilder + .fromUriString(kakaoProperties.getMapApiUrl()) + .queryParam("x", longitude) + .queryParam("y", latitude) + .build() + .toUri(); + MapApiResponse mapApiResponse = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(null, headers), MapApiResponse.class) + .getBody(); + return mapApiResponse.getDocuments().get(0).getAddress(); + } catch (Exception e) { + log.error("Location Provider occurs error! {}", e.getMessage()); + return ""; + } } } From bf2de1ee38e270f7eb47d69e550ab266f03e06a5 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Tue, 14 Nov 2023 22:31:27 +0900 Subject: [PATCH 25/37] =?UTF-8?q?Feat(#54):=20improvement=20community=20ap?= =?UTF-8?q?is=20-=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C,=20=EC=9E=91=EC=84=B1=EC=9E=90=EC=9D=98=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=A1=9C=EA=B9=85=20-=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=EC=97=90=20=EB=8C=93=EA=B8=80=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EC=8B=9C=20=ED=91=B8=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 2 +- .../dto/response/UploadArticleResponse.java | 2 +- .../domain/article/entity/Article.java | 5 ++++ .../custom/ArticleRepositoryCustomImpl.java | 4 ++- .../article/service/ArticleService.java | 21 +++++++++++--- .../fcm/service/FcmMessageProvider.java | 29 +++++++++++++++++++ .../notification/NotificationMessage.java | 16 ++++++++++ .../notification/NotificationMessageSpec.java | 7 +++++ 8 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/numberone/backend/support/notification/NotificationMessage.java create mode 100644 src/main/java/com/numberone/backend/support/notification/NotificationMessageSpec.java diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index dffd1d5a..fe01c2fa 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -119,6 +119,6 @@ public ResponseEntity modifyArticle( return ResponseEntity.ok(articleService.modifyArticle(articleId, request)); } - // todo: 작성자 위경도 주소 변환 처리, 게시글 좋아요, 게시글에 댓글 달리면 작성자에게 푸시알람 전송, 게시글 신고 기능 + // todo: 게시글 좋아요, 게시글 신고 기능 } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java index f74e51a8..fef61aa5 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java @@ -29,7 +29,7 @@ public static UploadArticleResponse of(Article article, List imageUrls, .createdAt(article.getCreatedAt()) .imageUrls(imageUrls) .thumbNailImageUrl(thumbNailImageUrl) - .address("서울시 광진구 자양동") + .address(article.getAddress()) .build(); } diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index 7aa906ca..620befa2 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -71,6 +71,7 @@ public Article(String title, String content, Long articleOwnerId, ArticleTag tag this.articleOwnerId = articleOwnerId; this.articleTag = tag; this.articleStatus = ArticleStatus.ACTIVATED; + this.commentCount = 0; } public void updateArticleImage(List images, Long thumbNailImageUrlId) { @@ -88,4 +89,8 @@ public void modifyArticle(String title, String content, ArticleTag tag) { this.articleTag = tag; } + public void updateAddress(String address){ + this.address = address; + } + } diff --git a/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java b/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java index f134e3e7..f4160bad 100644 --- a/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java +++ b/src/main/java/com/numberone/backend/domain/article/repository/custom/ArticleRepositoryCustomImpl.java @@ -3,6 +3,7 @@ import com.numberone.backend.domain.article.dto.response.ArticleSearchParameter; import com.numberone.backend.domain.article.dto.response.GetArticleListResponse; import com.numberone.backend.domain.article.dto.response.QGetArticleListResponse; +import com.numberone.backend.domain.article.entity.ArticleStatus; import com.numberone.backend.domain.article.entity.ArticleTag; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -33,7 +34,8 @@ public Slice getArticlesNoOffSetPaging(ArticleSearchPara .from(article) .where( ltArticleId(param.getLastArticleId()), - checkTagCondition(param.getTag()) + checkTagCondition(param.getTag()), + article.articleStatus.eq(ArticleStatus.ACTIVATED) ) .orderBy(article.id.desc()) .limit(pageable.getPageSize() + 1) diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index 194517d9..265d4ec4 100644 --- a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -18,9 +18,10 @@ import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.token.util.SecurityContextProvider; import com.numberone.backend.exception.notfound.NotFoundArticleException; -import com.numberone.backend.exception.notfound.NotFoundArticleImageException; import com.numberone.backend.exception.notfound.NotFoundMemberException; +import com.numberone.backend.support.fcm.service.FcmMessageProvider; import com.numberone.backend.support.s3.S3Provider; +import com.numberone.backend.util.LocationProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; @@ -35,6 +36,8 @@ import java.util.Objects; import java.util.Optional; +import static com.numberone.backend.support.notification.NotificationMessage.ARTICLE_COMMENT_FCM_ALARM; + @Slf4j @RequiredArgsConstructor @Transactional(readOnly = true) @@ -47,6 +50,8 @@ public class ArticleService { private final ArticleImageRepository articleImageRepository; private final CommentRepository commentRepository; private final S3Provider s3Provider; + private final LocationProvider locationProvider; + private final FcmMessageProvider fcmMessageProvider; @Transactional public UploadArticleResponse uploadArticle(UploadArticleRequest request) { @@ -94,6 +99,14 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { // 3. 게시글 - 이미지 연관 관계 설정 article.updateArticleImage(articleImages, thumbNailImageId); + // 4. 작성자 주소 설정 + Double latitude = request.getLatitude(); + Double longitude = request.getLongitude(); + if (latitude != null && longitude != null) { + String address = locationProvider.pos2address(request.getLatitude(), request.getLongitude()); + article.updateAddress(address); + } + return UploadArticleResponse.of(article, imageUrls, thumbNailImageUrl); } @@ -130,7 +143,6 @@ public GetArticleDetailResponse getArticleDetail(Long articleId) { } public Slice getArticleListPaging(ArticleSearchParameter param, Pageable pageable) { - // todo: 게시글 상태 고려하여 조회하기 (삭제 여부) return new SliceImpl<>( articleRepository.getArticlesNoOffSetPaging(param, pageable) .stream() @@ -150,7 +162,7 @@ public void updateArticleInfo(GetArticleListResponse articleInfo) { } @Transactional - public CreateCommentResponse createComment(Long articleId, CreateCommentRequest request){ + public CreateCommentResponse createComment(Long articleId, CreateCommentRequest request) { String principal = SecurityContextProvider.getAuthenticatedUserEmail(); Member member = memberRepository.findByEmail(principal) .orElseThrow(NotFoundMemberException::new); @@ -161,12 +173,13 @@ public CreateCommentResponse createComment(Long articleId, CreateCommentRequest ); articleParticipantRepository.save(new ArticleParticipant(article, member)); + fcmMessageProvider.sendFcm(member, ARTICLE_COMMENT_FCM_ALARM); return CreateCommentResponse.of(savedComment); } @Transactional - public ModifyArticleResponse modifyArticle(Long articleId, ModifyArticleRequest request){ + public ModifyArticleResponse modifyArticle(Long articleId, ModifyArticleRequest request) { String principal = SecurityContextProvider.getAuthenticatedUserEmail(); Member member = memberRepository.findByEmail(principal) .orElseThrow(NotFoundMemberException::new); diff --git a/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java b/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java index 1284b7c4..594643a6 100644 --- a/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java +++ b/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java @@ -1,20 +1,48 @@ package com.numberone.backend.support.fcm.service; import com.google.firebase.messaging.*; +import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.exception.conflict.FirebaseMessageSendException; import com.numberone.backend.support.fcm.dto.FcmNotificationDto; +import com.numberone.backend.support.notification.NotificationMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.IntStream; @Slf4j @Component public class FcmMessageProvider { + public void sendFcm(Member member, NotificationMessage notificationMessage){ + String token = member.getFcmToken(); + if (Objects.isNull(token)){ + log.error("해당 회원의 fcm 토큰이 존재하지 않아, 푸시알람을 전송할 수 없습니다."); + return; + } + + Message message = Message.builder() + .putData("time", LocalDateTime.now().toString()) + .setNotification( + Notification.builder() + .setTitle(notificationMessage.getTitle()) + .setBody(notificationMessage.getBody()) + .build() + ) + .setToken(token) + .build(); + try { + String response = FirebaseMessaging.getInstance().send(message); + log.info("Fcm 푸시 알람을 전송하였습니다."); + } catch (Exception e){ + log.error("Fcm 푸시 알람을 전송하는 도중에 에러가 발생했습니다. {}", e.getMessage()); + } + } + public void sendFcmToMembers(List tokens, FcmNotificationDto fcmDto) { List messages = tokens.stream().map( token -> Message.builder() @@ -50,4 +78,5 @@ public void sendFcmToMembers(List tokens, FcmNotificationDto fcmDto) { throw new FirebaseMessageSendException(); } } + } diff --git a/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java b/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java new file mode 100644 index 00000000..8fdf8316 --- /dev/null +++ b/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java @@ -0,0 +1,16 @@ +package com.numberone.backend.support.notification; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NotificationMessage implements NotificationMessageSpec { + + ARTICLE_COMMENT_FCM_ALARM("[대피로 알림]", "게시글에 댓글이 달렸어요!", null); + + private final String title; + private final String body; + private final String imageUrl; + +} diff --git a/src/main/java/com/numberone/backend/support/notification/NotificationMessageSpec.java b/src/main/java/com/numberone/backend/support/notification/NotificationMessageSpec.java new file mode 100644 index 00000000..e9083410 --- /dev/null +++ b/src/main/java/com/numberone/backend/support/notification/NotificationMessageSpec.java @@ -0,0 +1,7 @@ +package com.numberone.backend.support.notification; + +public interface NotificationMessageSpec { + String getTitle(); + String getBody(); + String getImageUrl(); +} From 6094aa96fffb9e4b4ca31f172ae830c3ea793a97 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 01:37:48 +0900 Subject: [PATCH 26/37] =?UTF-8?q?Feat(#54):=20improvement=20community=20ap?= =?UTF-8?q?is=20-=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C,=20=EC=9E=91=EC=84=B1=EC=9E=90=EC=9D=98=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=A1=9C=EA=B9=85=20-=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=EC=97=90=20=EB=8C=93=EA=B8=80=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EC=8B=9C=20=ED=91=B8=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/dto/response/ArticleSearchParameter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java b/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java index 18da68f9..5eb8b6dd 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/ArticleSearchParameter.java @@ -11,4 +11,6 @@ public class ArticleSearchParameter { private ArticleTag tag; private Long lastArticleId; + private Double longitude; + private Double latitude; } From 2cab94019decd14501b69b96e157e7763df03f98 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 01:38:30 +0900 Subject: [PATCH 27/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=A2=8B=EC=95=84=EC=9A=94=20api=20(wip)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 10 +++++++ .../domain/article/entity/Article.java | 4 +++ .../like/repository/RedisLockRepository.java | 23 ++++++++++++++++ .../like/service/RedisLockLikeFacade.java | 26 +++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 src/main/java/com/numberone/backend/support/redis/like/repository/RedisLockRepository.java create mode 100644 src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index fe01c2fa..9eb25692 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -6,6 +6,7 @@ import com.numberone.backend.domain.article.service.ArticleService; import com.numberone.backend.domain.comment.dto.request.CreateCommentRequest; import com.numberone.backend.domain.comment.dto.response.CreateCommentResponse; +import com.numberone.backend.support.redis.like.service.RedisLockLikeFacade; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -24,6 +25,7 @@ public class ArticleController { private final ArticleService articleService; + private final RedisLockLikeFacade redisLockLikeFacade; @Operation(summary = "게시글 작성 API", description = """ @@ -119,6 +121,14 @@ public ResponseEntity modifyArticle( return ResponseEntity.ok(articleService.modifyArticle(articleId, request)); } + @PutMapping("{article-id}/like") + public ResponseEntity updateLikeCount( + @PathVariable("article-id") Long articleId + ) throws InterruptedException { + redisLockLikeFacade.increaseArticleLike(articleId); + return ResponseEntity.ok("up"); + } + // todo: 게시글 좋아요, 게시글 신고 기능 } diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index 620befa2..7544615c 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -93,4 +93,8 @@ public void updateAddress(String address){ this.address = address; } + public void increaseLikeCount(){ + this.likeCount++; + } + } diff --git a/src/main/java/com/numberone/backend/support/redis/like/repository/RedisLockRepository.java b/src/main/java/com/numberone/backend/support/redis/like/repository/RedisLockRepository.java new file mode 100644 index 00000000..8344e4ce --- /dev/null +++ b/src/main/java/com/numberone/backend/support/redis/like/repository/RedisLockRepository.java @@ -0,0 +1,23 @@ +package com.numberone.backend.support.redis.like.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +@RequiredArgsConstructor +public class RedisLockRepository { + private final RedisTemplate redisTemplate; + + public Boolean lock(Long key){ + return redisTemplate.opsForValue() + .setIfAbsent(key.toString(), "lock", Duration.ofMillis(3_000)); + } + + public Boolean unlock(Long key){ + return redisTemplate.delete(key.toString()); + } + +} diff --git a/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java b/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java new file mode 100644 index 00000000..98f7663f --- /dev/null +++ b/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java @@ -0,0 +1,26 @@ +package com.numberone.backend.support.redis.like.service; + +import com.numberone.backend.domain.article.service.ArticleService; +import com.numberone.backend.support.redis.like.repository.RedisLockRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RedisLockLikeFacade { + private final RedisLockRepository redisLockRepository; + + public void increaseArticleLike(Long articleId) throws InterruptedException { + while (!redisLockRepository.lock(articleId)){ + Thread.sleep(100); + } + try { + // todo: 좋아요 로직 + } catch (Exception e){ + throw new RuntimeException(); + } finally { + redisLockRepository.unlock(articleId); + } + } + +} From 2cef11bd1f451e6b0c11c68d645c90d21ae7fbd7 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 02:39:01 +0900 Subject: [PATCH 28/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=A2=8B=EC=95=84=EC=9A=94=20api=20(wip)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 10 +- .../domain/article/entity/Article.java | 11 +- .../comment/controller/CommentController.java | 2 +- .../domain/comment/entity/CommentEntity.java | 16 ++- .../like/controller/LikeController.java | 17 +++ .../domain/like/entity/ArticleLike.java | 33 ++++++ .../domain/like/entity/CommentLike.java | 33 ++++++ .../repository/ArticleLikeRepository.java | 13 +++ .../repository/CommentLikeRepository.java | 11 ++ .../domain/like/service/LikeService.java | 108 ++++++++++++++++++ .../backend/domain/member/entity/Member.java | 10 +- .../like/service/RedisLockLikeFacade.java | 3 +- 12 files changed, 249 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/like/controller/LikeController.java create mode 100644 src/main/java/com/numberone/backend/domain/like/entity/ArticleLike.java create mode 100644 src/main/java/com/numberone/backend/domain/like/entity/CommentLike.java create mode 100644 src/main/java/com/numberone/backend/domain/like/repository/ArticleLikeRepository.java create mode 100644 src/main/java/com/numberone/backend/domain/like/repository/CommentLikeRepository.java create mode 100644 src/main/java/com/numberone/backend/domain/like/service/LikeService.java diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index 9eb25692..a93be88d 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -87,7 +87,7 @@ public ResponseEntity getArticleDetails(@PathVariable( @GetMapping public ResponseEntity> getArticlePages( Pageable pageable, - @ModelAttribute ArticleSearchParameter param) { + @ModelAttribute ArticleSearchParameter param) { // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정 return ResponseEntity.ok(articleService.getArticleListPaging(param, pageable)); } @@ -121,14 +121,6 @@ public ResponseEntity modifyArticle( return ResponseEntity.ok(articleService.modifyArticle(articleId, request)); } - @PutMapping("{article-id}/like") - public ResponseEntity updateLikeCount( - @PathVariable("article-id") Long articleId - ) throws InterruptedException { - redisLockLikeFacade.increaseArticleLike(articleId); - return ResponseEntity.ok("up"); - } - // todo: 게시글 좋아요, 게시글 신고 기능 } diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index 7544615c..c0bea662 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -72,6 +72,7 @@ public Article(String title, String content, Long articleOwnerId, ArticleTag tag this.articleTag = tag; this.articleStatus = ArticleStatus.ACTIVATED; this.commentCount = 0; + this.likeCount = 0; } public void updateArticleImage(List images, Long thumbNailImageUrlId) { @@ -89,12 +90,18 @@ public void modifyArticle(String title, String content, ArticleTag tag) { this.articleTag = tag; } - public void updateAddress(String address){ + public void updateAddress(String address) { this.address = address; } - public void increaseLikeCount(){ + public void increaseLikeCount() { this.likeCount++; } + public void decreaseLikeCount() { + if (this.likeCount > 0) { + this.likeCount--; + } + } + } diff --git a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java index 3e3701de..b4996e40 100644 --- a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java +++ b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java @@ -56,7 +56,7 @@ public ResponseEntity createChildComment( """) @GetMapping("{article-id}") public ResponseEntity> getCommentsByArticle(@PathVariable("article-id") Long articleId){ - List response = commentService.getCommentsByArticle(articleId); + List response = commentService.getCommentsByArticle(articleId); // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정 return ResponseEntity.ok(response); } diff --git a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java index 7961af6f..91fb1299 100644 --- a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java +++ b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java @@ -5,7 +5,6 @@ import com.numberone.backend.domain.member.entity.Member; import jakarta.persistence.*; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; @@ -47,15 +46,26 @@ public class CommentEntity extends BaseTimeEntity { @OneToMany(mappedBy = "parent", orphanRemoval = true) private List childs = new ArrayList<>(); - public CommentEntity(String content, Article article, Member author){ + public CommentEntity(String content, Article article, Member author) { this.depth = 0; this.content = content; this.article = article; this.authorId = author.getId(); + this.likeCount = 0; } - public void updateParent(CommentEntity parent){ + public void updateParent(CommentEntity parent) { this.parent = parent; } + public void increaseLikeCount() { + this.likeCount++; + } + + public void decreaseLikeCount() { + if (this.likeCount > 0) { + this.likeCount--; + } + } + } diff --git a/src/main/java/com/numberone/backend/domain/like/controller/LikeController.java b/src/main/java/com/numberone/backend/domain/like/controller/LikeController.java new file mode 100644 index 00000000..1857a767 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/like/controller/LikeController.java @@ -0,0 +1,17 @@ +package com.numberone.backend.domain.like.controller; + +import com.numberone.backend.support.redis.like.service.RedisLockLikeFacade; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/likes") +public class LikeController { + private final RedisLockLikeFacade redisLockLikeFacade; + + +} diff --git a/src/main/java/com/numberone/backend/domain/like/entity/ArticleLike.java b/src/main/java/com/numberone/backend/domain/like/entity/ArticleLike.java new file mode 100644 index 00000000..53f0dbe9 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/like/entity/ArticleLike.java @@ -0,0 +1,33 @@ +package com.numberone.backend.domain.like.entity; + +import com.numberone.backend.config.basetime.BaseTimeEntity; +import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Comment("게시글 좋아요 정보") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +@Table(name = "ARTICLE_LIKE") +public class ArticleLike extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinColumn(name = "member_id") + private Member member; + + private Long articleId; + + public ArticleLike(Member member, Article article){ + this.member = member; + this.articleId = article.getId(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/like/entity/CommentLike.java b/src/main/java/com/numberone/backend/domain/like/entity/CommentLike.java new file mode 100644 index 00000000..36759115 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/like/entity/CommentLike.java @@ -0,0 +1,33 @@ +package com.numberone.backend.domain.like.entity; + +import com.numberone.backend.config.basetime.BaseTimeEntity; +import com.numberone.backend.domain.comment.entity.CommentEntity; +import com.numberone.backend.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Comment("댓글 좋아요 정보") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +@Table(name = "COMMENT_LIKE") +public class CommentLike extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinColumn(name = "member_id") + private Member member; + + private Long commentId; + + public CommentLike(Member member, CommentEntity comment) { + this.member = member; + this.commentId = comment.getId(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/like/repository/ArticleLikeRepository.java b/src/main/java/com/numberone/backend/domain/like/repository/ArticleLikeRepository.java new file mode 100644 index 00000000..390e5ecf --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/like/repository/ArticleLikeRepository.java @@ -0,0 +1,13 @@ +package com.numberone.backend.domain.like.repository; + +import com.numberone.backend.domain.like.entity.ArticleLike; +import com.numberone.backend.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ArticleLikeRepository extends JpaRepository { + + List findByMember(Member member); + +} diff --git a/src/main/java/com/numberone/backend/domain/like/repository/CommentLikeRepository.java b/src/main/java/com/numberone/backend/domain/like/repository/CommentLikeRepository.java new file mode 100644 index 00000000..c6ee3d8f --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/like/repository/CommentLikeRepository.java @@ -0,0 +1,11 @@ +package com.numberone.backend.domain.like.repository; + +import com.numberone.backend.domain.like.entity.CommentLike; +import com.numberone.backend.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CommentLikeRepository extends JpaRepository { + List findByMember(Member member); +} diff --git a/src/main/java/com/numberone/backend/domain/like/service/LikeService.java b/src/main/java/com/numberone/backend/domain/like/service/LikeService.java new file mode 100644 index 00000000..67fcd174 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/like/service/LikeService.java @@ -0,0 +1,108 @@ +package com.numberone.backend.domain.like.service; + +import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.article.repository.ArticleRepository; +import com.numberone.backend.domain.comment.entity.CommentEntity; +import com.numberone.backend.domain.comment.repository.CommentRepository; +import com.numberone.backend.domain.like.entity.ArticleLike; +import com.numberone.backend.domain.like.entity.CommentLike; +import com.numberone.backend.domain.like.repository.ArticleLikeRepository; +import com.numberone.backend.domain.like.repository.CommentLikeRepository; +import com.numberone.backend.domain.member.entity.Member; +import com.numberone.backend.domain.member.repository.MemberRepository; +import com.numberone.backend.domain.token.util.SecurityContextProvider; +import com.numberone.backend.exception.notfound.NotFoundApiException; +import com.numberone.backend.exception.notfound.NotFoundCommentException; +import com.numberone.backend.exception.notfound.NotFoundMemberException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class LikeService { + + private final ArticleLikeRepository articleLikeRepository; + private final ArticleRepository articleRepository; + private final CommentLikeRepository commentLikeRepository; + private final CommentRepository commentRepository; + private final MemberRepository memberRepository; + + + @Transactional + public void increaseArticleLike(Long articleId) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + Article article = articleRepository.findById(articleId) + .orElseThrow(NotFoundApiException::new); + if (isAlreadyLikedArticle(member, articleId)) { + // todo: 이미 좋아요를 누른 게시글입니다. + } + article.increaseLikeCount(); + articleLikeRepository.save(new ArticleLike(member, article)); + } + + @Transactional + public void decreaseArticleLike(Long articleId) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + Article article = articleRepository.findById(articleId) + .orElseThrow(NotFoundApiException::new); + if (!isAlreadyLikedArticle(member, articleId)) { + // todo: 좋아요를 누르지 않은 게시글이라 취소할 수 없습니다. + } + article.decreaseLikeCount(); + + // 사용자의 게시글 좋아요 목록에서 제거 + List articleLikeList = articleLikeRepository.findByMember(member); + articleLikeList.removeIf(articleLike -> articleLike.getArticleId().equals(articleId)); + } + + @Transactional + public void increaseCommentLike(Long commentId) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + CommentEntity commentEntity = commentRepository.findById(commentId) + .orElseThrow(NotFoundCommentException::new); + if (isAlreadyLikedComment(member, commentId)) { + // todo: 이미 좋아요를 누른 댓글입니다. + } + commentEntity.increaseLikeCount(); + commentLikeRepository.save(new CommentLike(member, commentEntity)); + } + + @Transactional + public void decreaseCommentLike(Long commentId) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + CommentEntity commentEntity = commentRepository.findById(commentId) + .orElseThrow(NotFoundCommentException::new); + if (!isAlreadyLikedComment(member, commentId)){ + // todo: 좋아요를 누르지 않은 댓글이라 좋아요를 취소할 수 없습니다. + } + commentEntity.decreaseLikeCount(); + // 사용자의 댓글 좋아요 목록에서 제거 + List commentLikeList = commentLikeRepository.findByMember(member); + commentLikeList.removeIf(commentLike -> commentLike.getCommentId().equals(commentId)); + } + + public boolean isAlreadyLikedArticle(Member member, Long articleId) { + return articleLikeRepository.findByMember(member).stream() + .anyMatch(articleLike -> articleLike.getArticleId().equals(articleId)); + } + + public boolean isAlreadyLikedComment(Member member, Long commentId) { + return commentLikeRepository.findByMember(member).stream() + .anyMatch(commentLike -> commentLike.getCommentId().equals(commentId)); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/member/entity/Member.java b/src/main/java/com/numberone/backend/domain/member/entity/Member.java index 299296f7..49451d4b 100644 --- a/src/main/java/com/numberone/backend/domain/member/entity/Member.java +++ b/src/main/java/com/numberone/backend/domain/member/entity/Member.java @@ -1,6 +1,8 @@ package com.numberone.backend.domain.member.entity; import com.numberone.backend.config.basetime.BaseTimeEntity; +import com.numberone.backend.domain.like.entity.ArticleLike; +import com.numberone.backend.domain.like.entity.CommentLike; import jakarta.persistence.*; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -44,6 +46,12 @@ public class Member extends BaseTimeEntity { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List supports = new ArrayList<>(); + @OneToMany(mappedBy = "member", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) + private List commentLikes = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) + private List articleLikes = new ArrayList<>(); + @Comment("회원 프로필 사진 URL") private String profileImageUrl; @@ -72,7 +80,7 @@ public static Member of(String email, String realName) { .build(); } - public void updateProfileImageUrl(String imageUrl){ + public void updateProfileImageUrl(String imageUrl) { this.profileImageUrl = imageUrl; } diff --git a/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java b/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java index 98f7663f..b0d49b5a 100644 --- a/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java +++ b/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java @@ -1,6 +1,5 @@ package com.numberone.backend.support.redis.like.service; -import com.numberone.backend.domain.article.service.ArticleService; import com.numberone.backend.support.redis.like.repository.RedisLockRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -11,7 +10,7 @@ public class RedisLockLikeFacade { private final RedisLockRepository redisLockRepository; public void increaseArticleLike(Long articleId) throws InterruptedException { - while (!redisLockRepository.lock(articleId)){ + while (!redisLockRepository.lock(articleId)) { Thread.sleep(100); } try { From 7569cda85f700d75c2dfd2d607ab45ec2f4ebfd9 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 19:14:47 +0900 Subject: [PATCH 29/37] =?UTF-8?q?Feat(#54):=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=B0=9C=EC=86=A1=20=EC=8B=9C=20db=20?= =?UTF-8?q?=EC=97=90=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/NotificationEntity.java | 53 +++++++++++++++++++ .../notification/entity/NotificationTag.java | 12 +++++ .../repository/NotificationRepository.java | 7 +++ .../fcm/service/FcmMessageProvider.java | 20 +++++-- 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/notification/entity/NotificationEntity.java create mode 100644 src/main/java/com/numberone/backend/domain/notification/entity/NotificationTag.java create mode 100644 src/main/java/com/numberone/backend/domain/notification/repository/NotificationRepository.java diff --git a/src/main/java/com/numberone/backend/domain/notification/entity/NotificationEntity.java b/src/main/java/com/numberone/backend/domain/notification/entity/NotificationEntity.java new file mode 100644 index 00000000..8d82a283 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/notification/entity/NotificationEntity.java @@ -0,0 +1,53 @@ +package com.numberone.backend.domain.notification.entity; + +import com.numberone.backend.config.basetime.BaseTimeEntity; +import com.numberone.backend.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Comment("알림 정보") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +@Table(name = "NOTIFICATION") +public class NotificationEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Comment("알림 대상 회원의 ID") + private Long receivedMemberId; + + @Comment("알림 대상 회원의 닉네임") + private String nickName; + + @Comment("알림 종류") + @Enumerated(EnumType.STRING) + private NotificationTag notificationTag; + + @Comment("알림 제목") + private String title; + + @Comment("알림 내용") + private String body; + + @Comment("알림 정상 전송 여부") + private Boolean isSent; + + @Comment("확인한 알림인지 여부") + private Boolean isRead; + + public NotificationEntity(Member member, NotificationTag tag, String title, String body, Boolean isSent) { + this.receivedMemberId = member.getId(); + this.notificationTag = tag; + this.title = title; + this.body = body; + this.isSent = isSent; + isRead = false; + } + +} diff --git a/src/main/java/com/numberone/backend/domain/notification/entity/NotificationTag.java b/src/main/java/com/numberone/backend/domain/notification/entity/NotificationTag.java new file mode 100644 index 00000000..64614cdd --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/notification/entity/NotificationTag.java @@ -0,0 +1,12 @@ +package com.numberone.backend.domain.notification.entity; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum NotificationTag { + COMMUNITY, // 커뮤니티 + SUPPORT, // 후원 + FAMILY, // 가족 + ; + private String value; +} diff --git a/src/main/java/com/numberone/backend/domain/notification/repository/NotificationRepository.java b/src/main/java/com/numberone/backend/domain/notification/repository/NotificationRepository.java new file mode 100644 index 00000000..1ac16808 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/notification/repository/NotificationRepository.java @@ -0,0 +1,7 @@ +package com.numberone.backend.domain.notification.repository; + +import com.numberone.backend.domain.notification.entity.NotificationEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NotificationRepository extends JpaRepository { +} diff --git a/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java b/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java index 594643a6..5bfff921 100644 --- a/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java +++ b/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java @@ -2,9 +2,13 @@ import com.google.firebase.messaging.*; import com.numberone.backend.domain.member.entity.Member; +import com.numberone.backend.domain.notification.entity.NotificationEntity; +import com.numberone.backend.domain.notification.entity.NotificationTag; +import com.numberone.backend.domain.notification.repository.NotificationRepository; import com.numberone.backend.exception.conflict.FirebaseMessageSendException; import com.numberone.backend.support.fcm.dto.FcmNotificationDto; import com.numberone.backend.support.notification.NotificationMessage; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -16,29 +20,35 @@ @Slf4j @Component +@RequiredArgsConstructor public class FcmMessageProvider { - - public void sendFcm(Member member, NotificationMessage notificationMessage){ + private final NotificationRepository notificationRepository; + public void sendFcm(Member member, NotificationMessage notificationMessage, NotificationTag tag){ String token = member.getFcmToken(); if (Objects.isNull(token)){ log.error("해당 회원의 fcm 토큰이 존재하지 않아, 푸시알람을 전송할 수 없습니다."); return; } + String title = notificationMessage.getTitle(); + String body = member.getNickName() + "님, " + notificationMessage.getBody(); + Message message = Message.builder() .putData("time", LocalDateTime.now().toString()) .setNotification( Notification.builder() - .setTitle(notificationMessage.getTitle()) - .setBody(notificationMessage.getBody()) + .setTitle(title) + .setBody(body) .build() ) .setToken(token) .build(); try { String response = FirebaseMessaging.getInstance().send(message); - log.info("Fcm 푸시 알람을 전송하였습니다."); + notificationRepository.save(new NotificationEntity(member, tag, title, body, true)); + log.info("Fcm 푸시 알람을 성공적으로 전송하였습니다."); } catch (Exception e){ + notificationRepository.save(new NotificationEntity(member, tag, title, body, false)); log.error("Fcm 푸시 알람을 전송하는 도중에 에러가 발생했습니다. {}", e.getMessage()); } } From 5cf27503eeec82bd57f80fde24ff26670ce41fee Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 19:17:40 +0900 Subject: [PATCH 30/37] =?UTF-8?q?Feat(#54):=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=ED=83=9C=EA=B7=B8=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/article/service/ArticleService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index 265d4ec4..c2c5d312 100644 --- a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -16,6 +16,7 @@ import com.numberone.backend.domain.comment.repository.CommentRepository; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; +import com.numberone.backend.domain.notification.entity.NotificationTag; import com.numberone.backend.domain.token.util.SecurityContextProvider; import com.numberone.backend.exception.notfound.NotFoundArticleException; import com.numberone.backend.exception.notfound.NotFoundMemberException; @@ -173,7 +174,7 @@ public CreateCommentResponse createComment(Long articleId, CreateCommentRequest ); articleParticipantRepository.save(new ArticleParticipant(article, member)); - fcmMessageProvider.sendFcm(member, ARTICLE_COMMENT_FCM_ALARM); + fcmMessageProvider.sendFcm(member, ARTICLE_COMMENT_FCM_ALARM, NotificationTag.COMMUNITY); return CreateCommentResponse.of(savedComment); } From 2996aca629f9998e06762b4f9eddd6f4eb0e5bea Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 19:24:28 +0900 Subject: [PATCH 31/37] =?UTF-8?q?Feat(#54):=20=EC=A2=8B=EC=95=84=EC=9A=94?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/like/service/LikeService.java | 14 ++++++++++---- .../exception/conflict/AlreadyLikedException.java | 9 +++++++++ .../conflict/AlreadyUnLikedException.java | 9 +++++++++ .../exception/context/CustomExceptionContext.java | 4 ++++ 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/numberone/backend/exception/conflict/AlreadyLikedException.java create mode 100644 src/main/java/com/numberone/backend/exception/conflict/AlreadyUnLikedException.java diff --git a/src/main/java/com/numberone/backend/domain/like/service/LikeService.java b/src/main/java/com/numberone/backend/domain/like/service/LikeService.java index 67fcd174..7dc69d7d 100644 --- a/src/main/java/com/numberone/backend/domain/like/service/LikeService.java +++ b/src/main/java/com/numberone/backend/domain/like/service/LikeService.java @@ -11,6 +11,8 @@ import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.token.util.SecurityContextProvider; +import com.numberone.backend.exception.conflict.AlreadyLikedException; +import com.numberone.backend.exception.conflict.AlreadyUnLikedException; import com.numberone.backend.exception.notfound.NotFoundApiException; import com.numberone.backend.exception.notfound.NotFoundCommentException; import com.numberone.backend.exception.notfound.NotFoundMemberException; @@ -42,7 +44,8 @@ public void increaseArticleLike(Long articleId) { Article article = articleRepository.findById(articleId) .orElseThrow(NotFoundApiException::new); if (isAlreadyLikedArticle(member, articleId)) { - // todo: 이미 좋아요를 누른 게시글입니다. + // 이미 좋아요를 누른 게시글입니다. + throw new AlreadyLikedException(); } article.increaseLikeCount(); articleLikeRepository.save(new ArticleLike(member, article)); @@ -56,7 +59,8 @@ public void decreaseArticleLike(Long articleId) { Article article = articleRepository.findById(articleId) .orElseThrow(NotFoundApiException::new); if (!isAlreadyLikedArticle(member, articleId)) { - // todo: 좋아요를 누르지 않은 게시글이라 취소할 수 없습니다. + // 좋아요를 누르지 않은 게시글이라 취소할 수 없습니다. + throw new AlreadyUnLikedException(); } article.decreaseLikeCount(); @@ -73,7 +77,8 @@ public void increaseCommentLike(Long commentId) { CommentEntity commentEntity = commentRepository.findById(commentId) .orElseThrow(NotFoundCommentException::new); if (isAlreadyLikedComment(member, commentId)) { - // todo: 이미 좋아요를 누른 댓글입니다. + // 이미 좋아요를 누른 댓글입니다. + throw new AlreadyLikedException(); } commentEntity.increaseLikeCount(); commentLikeRepository.save(new CommentLike(member, commentEntity)); @@ -87,7 +92,8 @@ public void decreaseCommentLike(Long commentId) { CommentEntity commentEntity = commentRepository.findById(commentId) .orElseThrow(NotFoundCommentException::new); if (!isAlreadyLikedComment(member, commentId)){ - // todo: 좋아요를 누르지 않은 댓글이라 좋아요를 취소할 수 없습니다. + // 좋아요를 누르지 않은 댓글이라 좋아요를 취소할 수 없습니다. + throw new AlreadyUnLikedException(); } commentEntity.decreaseLikeCount(); // 사용자의 댓글 좋아요 목록에서 제거 diff --git a/src/main/java/com/numberone/backend/exception/conflict/AlreadyLikedException.java b/src/main/java/com/numberone/backend/exception/conflict/AlreadyLikedException.java new file mode 100644 index 00000000..ece8d2d3 --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/conflict/AlreadyLikedException.java @@ -0,0 +1,9 @@ +package com.numberone.backend.exception.conflict; + +import static com.numberone.backend.exception.context.CustomExceptionContext.ALREADY_LIKED_ERROR; + +public class AlreadyLikedException extends ConflictException { + public AlreadyLikedException(){ + super(ALREADY_LIKED_ERROR); + } +} diff --git a/src/main/java/com/numberone/backend/exception/conflict/AlreadyUnLikedException.java b/src/main/java/com/numberone/backend/exception/conflict/AlreadyUnLikedException.java new file mode 100644 index 00000000..c0b3584a --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/conflict/AlreadyUnLikedException.java @@ -0,0 +1,9 @@ +package com.numberone.backend.exception.conflict; + +import static com.numberone.backend.exception.context.CustomExceptionContext.ALREADY_UNLIKED_ERROR; + +public class AlreadyUnLikedException extends ConflictException { + public AlreadyUnLikedException() { + super(ALREADY_UNLIKED_ERROR); + } +} diff --git a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java index cf99bf30..13eca97d 100644 --- a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java +++ b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java @@ -46,6 +46,10 @@ public enum CustomExceptionContext implements ExceptionContext { // comment 관련 예외 NOT_FOUND_COMMENT("해당 댓글을 찾을 수 없습니다.", 10000), + + // like 관련 예외 + ALREADY_LIKED_ERROR("이미 좋아요 처리된 엔티티입니다.", 11000), + ALREADY_UNLIKED_ERROR("이미 좋아요 해제 처리된 엔티티입니다.", 11001), ; private final String message; From 394b65e57e16442a887547047f99e3dcb931c353 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 22:00:02 +0900 Subject: [PATCH 32/37] =?UTF-8?q?Feat(#54):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=EB=8F=84=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/notification/entity/NotificationEntity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/numberone/backend/domain/notification/entity/NotificationEntity.java b/src/main/java/com/numberone/backend/domain/notification/entity/NotificationEntity.java index 8d82a283..67d000aa 100644 --- a/src/main/java/com/numberone/backend/domain/notification/entity/NotificationEntity.java +++ b/src/main/java/com/numberone/backend/domain/notification/entity/NotificationEntity.java @@ -43,6 +43,7 @@ public class NotificationEntity extends BaseTimeEntity { public NotificationEntity(Member member, NotificationTag tag, String title, String body, Boolean isSent) { this.receivedMemberId = member.getId(); + this.nickName = member.getNickName(); this.notificationTag = tag; this.title = title; this.body = body; From 2079a27e6690ac971429ddfa8539bc09150764a7 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 22:22:51 +0900 Subject: [PATCH 33/37] =?UTF-8?q?Feat(#54):=20=EA=B2=8C=EC=8B=9C=EA=B8=80/?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=A2=8B=EC=95=84=EC=9A=94=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../like/controller/LikeController.java | 35 +++++++++- .../response/ArticleLikeResponse.java | 13 ++++ .../response/CommentLikeResponse.java | 13 ++++ .../domain/like/service/LikeService.java | 20 ++++-- .../like/service/RedisLockLikeFacade.java | 66 ++++++++++++++++++- 5 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/like/dto/response/response/ArticleLikeResponse.java create mode 100644 src/main/java/com/numberone/backend/domain/like/dto/response/response/CommentLikeResponse.java diff --git a/src/main/java/com/numberone/backend/domain/like/controller/LikeController.java b/src/main/java/com/numberone/backend/domain/like/controller/LikeController.java index 1857a767..7a2ba262 100644 --- a/src/main/java/com/numberone/backend/domain/like/controller/LikeController.java +++ b/src/main/java/com/numberone/backend/domain/like/controller/LikeController.java @@ -1,10 +1,13 @@ package com.numberone.backend.domain.like.controller; +import com.numberone.backend.domain.like.dto.response.response.ArticleLikeResponse; +import com.numberone.backend.domain.like.dto.response.response.CommentLikeResponse; import com.numberone.backend.support.redis.like.service.RedisLockLikeFacade; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; @Slf4j @RestController @@ -13,5 +16,33 @@ public class LikeController { private final RedisLockLikeFacade redisLockLikeFacade; + @Operation(summary = "게시글 좋아요 추가") + @PutMapping("articles/{article-id}/add") + public ResponseEntity addArticleLike( + @PathVariable("article-id") Long articleId) throws InterruptedException { + return ResponseEntity.ok(redisLockLikeFacade.increaseArticleLike(articleId)); + } + + @Operation(summary = "게시글 좋아요 취소") + @PutMapping("articles/{article-id}/cancel") + public ResponseEntity cancelArticleLike( + @PathVariable("article-id") Long articleId) throws InterruptedException { + return ResponseEntity.ok(redisLockLikeFacade.decreaseArticleLike(articleId)); + } + + @Operation(summary = "댓글 좋아요 추가") + @PutMapping("comments/{comment-id}/add") + public ResponseEntity addCommentLike( + @PathVariable("comment-id") Long commentId) throws InterruptedException { + return ResponseEntity.ok(redisLockLikeFacade.increaseCommentLike(commentId)); + } + + @Operation(summary = "댓글 좋아요 취소") + @PutMapping("comments/{comment-id}/cancel") + public ResponseEntity cancelCommentLike( + @PathVariable("comment-id") Long commentId) throws InterruptedException { + return ResponseEntity.ok(redisLockLikeFacade.decreaseCommentLike(commentId)); + } + } diff --git a/src/main/java/com/numberone/backend/domain/like/dto/response/response/ArticleLikeResponse.java b/src/main/java/com/numberone/backend/domain/like/dto/response/response/ArticleLikeResponse.java new file mode 100644 index 00000000..6e66a9e2 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/like/dto/response/response/ArticleLikeResponse.java @@ -0,0 +1,13 @@ +package com.numberone.backend.domain.like.dto.response.response; + +import lombok.*; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ArticleLikeResponse { + private Long articleId; + private Integer currentLikeCount; +} diff --git a/src/main/java/com/numberone/backend/domain/like/dto/response/response/CommentLikeResponse.java b/src/main/java/com/numberone/backend/domain/like/dto/response/response/CommentLikeResponse.java new file mode 100644 index 00000000..2c41c8d2 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/like/dto/response/response/CommentLikeResponse.java @@ -0,0 +1,13 @@ +package com.numberone.backend.domain.like.dto.response.response; + +import lombok.*; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class CommentLikeResponse { + private Long commentId; + private Integer currentLikeCount; +} diff --git a/src/main/java/com/numberone/backend/domain/like/service/LikeService.java b/src/main/java/com/numberone/backend/domain/like/service/LikeService.java index 7dc69d7d..c43c90d7 100644 --- a/src/main/java/com/numberone/backend/domain/like/service/LikeService.java +++ b/src/main/java/com/numberone/backend/domain/like/service/LikeService.java @@ -37,7 +37,7 @@ public class LikeService { @Transactional - public void increaseArticleLike(Long articleId) { + public Integer increaseArticleLike(Long articleId) { String principal = SecurityContextProvider.getAuthenticatedUserEmail(); Member member = memberRepository.findByEmail(principal) .orElseThrow(NotFoundMemberException::new); @@ -49,10 +49,12 @@ public void increaseArticleLike(Long articleId) { } article.increaseLikeCount(); articleLikeRepository.save(new ArticleLike(member, article)); + + return article.getLikeCount(); } @Transactional - public void decreaseArticleLike(Long articleId) { + public Integer decreaseArticleLike(Long articleId) { String principal = SecurityContextProvider.getAuthenticatedUserEmail(); Member member = memberRepository.findByEmail(principal) .orElseThrow(NotFoundMemberException::new); @@ -67,10 +69,12 @@ public void decreaseArticleLike(Long articleId) { // 사용자의 게시글 좋아요 목록에서 제거 List articleLikeList = articleLikeRepository.findByMember(member); articleLikeList.removeIf(articleLike -> articleLike.getArticleId().equals(articleId)); + + return article.getLikeCount(); } @Transactional - public void increaseCommentLike(Long commentId) { + public Integer increaseCommentLike(Long commentId) { String principal = SecurityContextProvider.getAuthenticatedUserEmail(); Member member = memberRepository.findByEmail(principal) .orElseThrow(NotFoundMemberException::new); @@ -82,10 +86,12 @@ public void increaseCommentLike(Long commentId) { } commentEntity.increaseLikeCount(); commentLikeRepository.save(new CommentLike(member, commentEntity)); + + return commentEntity.getLikeCount(); } @Transactional - public void decreaseCommentLike(Long commentId) { + public Integer decreaseCommentLike(Long commentId) { String principal = SecurityContextProvider.getAuthenticatedUserEmail(); Member member = memberRepository.findByEmail(principal) .orElseThrow(NotFoundMemberException::new); @@ -99,14 +105,16 @@ public void decreaseCommentLike(Long commentId) { // 사용자의 댓글 좋아요 목록에서 제거 List commentLikeList = commentLikeRepository.findByMember(member); commentLikeList.removeIf(commentLike -> commentLike.getCommentId().equals(commentId)); + + return commentEntity.getLikeCount(); } - public boolean isAlreadyLikedArticle(Member member, Long articleId) { + private boolean isAlreadyLikedArticle(Member member, Long articleId) { return articleLikeRepository.findByMember(member).stream() .anyMatch(articleLike -> articleLike.getArticleId().equals(articleId)); } - public boolean isAlreadyLikedComment(Member member, Long commentId) { + private boolean isAlreadyLikedComment(Member member, Long commentId) { return commentLikeRepository.findByMember(member).stream() .anyMatch(commentLike -> commentLike.getCommentId().equals(commentId)); } diff --git a/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java b/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java index b0d49b5a..ff17894a 100644 --- a/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java +++ b/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java @@ -1,5 +1,8 @@ package com.numberone.backend.support.redis.like.service; +import com.numberone.backend.domain.like.dto.response.response.ArticleLikeResponse; +import com.numberone.backend.domain.like.dto.response.response.CommentLikeResponse; +import com.numberone.backend.domain.like.service.LikeService; import com.numberone.backend.support.redis.like.repository.RedisLockRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -8,18 +11,75 @@ @RequiredArgsConstructor public class RedisLockLikeFacade { private final RedisLockRepository redisLockRepository; + private final LikeService likeService; - public void increaseArticleLike(Long articleId) throws InterruptedException { + public ArticleLikeResponse increaseArticleLike(Long articleId) throws InterruptedException { while (!redisLockRepository.lock(articleId)) { Thread.sleep(100); } try { - // todo: 좋아요 로직 - } catch (Exception e){ + Integer likeCount = likeService.increaseArticleLike(articleId); + return ArticleLikeResponse.builder() + .currentLikeCount(likeCount) + .articleId(articleId) + .build(); + } catch (Exception e) { throw new RuntimeException(); } finally { redisLockRepository.unlock(articleId); } } + public ArticleLikeResponse decreaseArticleLike(Long articleId) throws InterruptedException { + while (!redisLockRepository.lock(articleId)) { + Thread.sleep(100); + } + try { + Integer likeCount = likeService.decreaseArticleLike(articleId); + return ArticleLikeResponse.builder() + .currentLikeCount(likeCount) + .articleId(articleId) + .build(); + } catch (Exception e) { + throw new RuntimeException(); + } finally { + redisLockRepository.unlock(articleId); + } + } + + public CommentLikeResponse increaseCommentLike(Long commentId) throws InterruptedException { + while (!redisLockRepository.lock(commentId)) { + Thread.sleep(100); + } + try { + Integer likeCount = likeService.increaseCommentLike(commentId); + return CommentLikeResponse.builder() + .currentLikeCount(likeCount) + .commentId(commentId) + .build(); + } catch (Exception e) { + throw new RuntimeException(); + } finally { + redisLockRepository.unlock(commentId); + } + } + + public CommentLikeResponse decreaseCommentLike(Long commentId) throws InterruptedException { + while (!redisLockRepository.lock(commentId)) { + Thread.sleep(100); + } + try { + Integer likeCount = likeService.decreaseCommentLike(commentId); + return CommentLikeResponse.builder() + .currentLikeCount(likeCount) + .commentId(commentId) + .build(); + } catch (Exception e) { + throw new RuntimeException(); + } finally { + redisLockRepository.unlock(commentId); + } + } + + } From 68c888438332a1b0d3e1b9b7158a211c38e81b05 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 22:39:53 +0900 Subject: [PATCH 34/37] =?UTF-8?q?Fix(#54):=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=20jwt=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/numberone/backend/config/SwaggerConfig.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/numberone/backend/config/SwaggerConfig.java b/src/main/java/com/numberone/backend/config/SwaggerConfig.java index 86d6d2b2..a1f07ee7 100644 --- a/src/main/java/com/numberone/backend/config/SwaggerConfig.java +++ b/src/main/java/com/numberone/backend/config/SwaggerConfig.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.servers.Server; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -27,10 +28,14 @@ ) @Configuration public class SwaggerConfig { + @Bean public OpenAPI customOpenAPI() { return new OpenAPI() - .components(new Components().addSecuritySchemes("JWT", bearerAuth())); + .components( + new Components() + .addSecuritySchemes("JWT", bearerAuth())) + .addSecurityItem(new SecurityRequirement().addList("JWT")); } public SecurityScheme bearerAuth() { @@ -41,4 +46,5 @@ public SecurityScheme bearerAuth() { .in(SecurityScheme.In.HEADER) .name(HttpHeaders.AUTHORIZATION); } + } From b622befe1194fcea95cdde7410bf3346f3d780a2 Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 22:45:22 +0900 Subject: [PATCH 35/37] =?UTF-8?q?Fix(#54):=20=EC=A2=8B=EC=95=84=EC=9A=94?= =?UTF-8?q?=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/redis/like/service/RedisLockLikeFacade.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java b/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java index ff17894a..13c82a57 100644 --- a/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java +++ b/src/main/java/com/numberone/backend/support/redis/like/service/RedisLockLikeFacade.java @@ -23,8 +23,6 @@ public ArticleLikeResponse increaseArticleLike(Long articleId) throws Interrupte .currentLikeCount(likeCount) .articleId(articleId) .build(); - } catch (Exception e) { - throw new RuntimeException(); } finally { redisLockRepository.unlock(articleId); } @@ -40,8 +38,6 @@ public ArticleLikeResponse decreaseArticleLike(Long articleId) throws Interrupte .currentLikeCount(likeCount) .articleId(articleId) .build(); - } catch (Exception e) { - throw new RuntimeException(); } finally { redisLockRepository.unlock(articleId); } @@ -57,8 +53,6 @@ public CommentLikeResponse increaseCommentLike(Long commentId) throws Interrupte .currentLikeCount(likeCount) .commentId(commentId) .build(); - } catch (Exception e) { - throw new RuntimeException(); } finally { redisLockRepository.unlock(commentId); } @@ -74,8 +68,6 @@ public CommentLikeResponse decreaseCommentLike(Long commentId) throws Interrupte .currentLikeCount(likeCount) .commentId(commentId) .build(); - } catch (Exception e) { - throw new RuntimeException(); } finally { redisLockRepository.unlock(commentId); } From cb7659bb19c1bc0d88b1eacff08b3a0a97ab7a8b Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 22:55:59 +0900 Subject: [PATCH 36/37] Docs(#54): update todo --- .../backend/domain/article/controller/ArticleController.java | 2 +- .../backend/domain/comment/controller/CommentController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index a93be88d..9bcb9151 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -121,6 +121,6 @@ public ResponseEntity modifyArticle( return ResponseEntity.ok(articleService.modifyArticle(articleId, request)); } - // todo: 게시글 좋아요, 게시글 신고 기능 + // todo: 게시글 신고 기능 } diff --git a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java index b4996e40..6ccf4158 100644 --- a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java +++ b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java @@ -60,6 +60,6 @@ public ResponseEntity> getCommentsByArticle(@PathVariable("a return ResponseEntity.ok(response); } - // todo: 댓글 삭제, 댓글 좋아요, 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능 + // todo: 댓글 삭제, 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능 } From 7ceadafa598c3960223cd3a369b7fd92404a936e Mon Sep 17 00:00:00 2001 From: Jaehyeon Date: Wed, 15 Nov 2023 23:32:28 +0900 Subject: [PATCH 37/37] Feat(#54): rename field for resolve rebase conflict --- .../com/numberone/backend/domain/member/entity/Member.java | 5 ++--- .../backend/domain/member/service/MemberService.java | 7 +++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/numberone/backend/domain/member/entity/Member.java b/src/main/java/com/numberone/backend/domain/member/entity/Member.java index 49451d4b..873322c0 100644 --- a/src/main/java/com/numberone/backend/domain/member/entity/Member.java +++ b/src/main/java/com/numberone/backend/domain/member/entity/Member.java @@ -11,7 +11,6 @@ import com.numberone.backend.domain.notificationdisaster.entity.NotificationDisaster; import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; import com.numberone.backend.domain.support.entity.Support; -import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -84,8 +83,8 @@ public void updateProfileImageUrl(String imageUrl) { this.profileImageUrl = imageUrl; } - public void updateNickname(String nickname) { - this.nickName = nickname; + public void updateNickName(String nickName) { + this.nickName = nickName; } public void plusHeart(int heart) { diff --git a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java index 03d41210..bca50289 100644 --- a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java +++ b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java @@ -18,7 +18,6 @@ import com.numberone.backend.support.s3.S3Provider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -39,15 +38,15 @@ public Member findByEmail(String email) { } @Transactional - public void create(String email, String realname) { - memberRepository.save(Member.of(email, realname)); + public void create(String email, String realName) { + memberRepository.save(Member.of(email, realName)); } @Transactional public void initMemberData(String email, OnboardingRequest onboardingRequest) { Member member = memberRepository.findByEmail(email) .orElseThrow(NotFoundMemberException::new); - member.updateNickname(onboardingRequest.getNickname()); + member.updateNickName(onboardingRequest.getNickname()); notificationDisasterRepository.deleteAllByMemberId(member.getId()); notificationRegionRepository.deleteAllByMemberId(member.getId()); for (OnboardingAddress address : onboardingRequest.getAddresses()) {