From f7386986c4d5b8e65ddf0c9c3a3cb73076f8668f Mon Sep 17 00:00:00 2001 From: nohy6630 Date: Fri, 17 Nov 2023 01:55:50 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Feat(#55):=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/context/CustomExceptionContext.java | 4 ++++ .../notfound/NotFoundConversationException.java | 12 ++++++++++++ .../notfound/NotFoundDisasterException.java | 11 +++++++++++ 3 files changed, 27 insertions(+) create mode 100644 src/main/java/com/numberone/backend/exception/notfound/NotFoundConversationException.java create mode 100644 src/main/java/com/numberone/backend/exception/notfound/NotFoundDisasterException.java 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 13eca97d..d0988aa2 100644 --- a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java +++ b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java @@ -28,6 +28,7 @@ public enum CustomExceptionContext implements ExceptionContext { INVALID_DISASTER_TYPE("존재하지 않는 재난 유형입니다.",5000), NOT_FOUND_API("데이터 수집을 위한 API 요청이 실패했습니다.",5001), NOT_FOUND_CRAWLING("데이터 수집을 위한 크롤링이 실패했습니다.",5002), + NOT_FOUND_DISASTER("존재하지 않는 재난상황입니다.", 5003), // fcm 관련 예외 FIREBASE_INITIALIZATION_FAILED("Firebase Application 초기 설정에 실패하였습니다.", 6000), @@ -50,6 +51,9 @@ public enum CustomExceptionContext implements ExceptionContext { // like 관련 예외 ALREADY_LIKED_ERROR("이미 좋아요 처리된 엔티티입니다.", 11000), ALREADY_UNLIKED_ERROR("이미 좋아요 해제 처리된 엔티티입니다.", 11001), + + //conversation 관련 예외 + NOT_FOUND_CONVERSATION("해당 대화를 찾을 수 없습니다.", 12000) ; private final String message; diff --git a/src/main/java/com/numberone/backend/exception/notfound/NotFoundConversationException.java b/src/main/java/com/numberone/backend/exception/notfound/NotFoundConversationException.java new file mode 100644 index 00000000..8ca37e93 --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/notfound/NotFoundConversationException.java @@ -0,0 +1,12 @@ +package com.numberone.backend.exception.notfound; + +import com.numberone.backend.exception.context.ExceptionContext; + +import static com.numberone.backend.exception.context.CustomExceptionContext.NOT_FOUND_CONVERSATION; + +public class NotFoundConversationException extends NotFoundException { + + public NotFoundConversationException() { + super(NOT_FOUND_CONVERSATION); + } +} diff --git a/src/main/java/com/numberone/backend/exception/notfound/NotFoundDisasterException.java b/src/main/java/com/numberone/backend/exception/notfound/NotFoundDisasterException.java new file mode 100644 index 00000000..c58872f1 --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/notfound/NotFoundDisasterException.java @@ -0,0 +1,11 @@ +package com.numberone.backend.exception.notfound; + +import com.numberone.backend.exception.context.ExceptionContext; + +import static com.numberone.backend.exception.context.CustomExceptionContext.NOT_FOUND_DISASTER; + +public class NotFoundDisasterException extends NotFoundException { + public NotFoundDisasterException() { + super(NOT_FOUND_DISASTER); + } +} From 6227297ab4585b3544d57a34417e1b72a071a8a1 Mon Sep 17 00:00:00 2001 From: nohy6630 Date: Fri, 17 Nov 2023 01:56:40 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Feat(#55):=20=EC=9E=AC=EB=82=9C=EC=83=81?= =?UTF-8?q?=ED=99=A9=20=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=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 --- .../controller/ConversationController.java | 73 +++++++++++ .../CreateChildConversationRequest.java | 15 +++ .../request/CreateConversationRequest.java | 20 +++ .../dto/response/GetConversationResponse.java | 60 +++++++++ .../conversation/entity/Conversation.java | 78 ++++++++++++ .../repository/ConversationRepository.java | 8 ++ .../service/ConversationService.java | 120 ++++++++++++++++++ .../controller/DisasterController.java | 10 ++ .../dto/response/SituationHomeResponse.java | 24 ++++ .../dto/response/SituationResponse.java | 54 ++++++++ .../domain/disaster/entity/Disaster.java | 5 + .../repository/DisasterRepository.java | 2 +- .../disaster/service/DisasterService.java | 47 ++++++- .../domain/like/entity/ConversationLike.java | 39 ++++++ .../ConversationLikeRepository.java | 15 +++ .../backend/domain/member/entity/Member.java | 4 + 16 files changed, 571 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/conversation/controller/ConversationController.java create mode 100644 src/main/java/com/numberone/backend/domain/conversation/dto/request/CreateChildConversationRequest.java create mode 100644 src/main/java/com/numberone/backend/domain/conversation/dto/request/CreateConversationRequest.java create mode 100644 src/main/java/com/numberone/backend/domain/conversation/dto/response/GetConversationResponse.java create mode 100644 src/main/java/com/numberone/backend/domain/conversation/entity/Conversation.java create mode 100644 src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java create mode 100644 src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java create mode 100644 src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationHomeResponse.java create mode 100644 src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationResponse.java create mode 100644 src/main/java/com/numberone/backend/domain/like/entity/ConversationLike.java create mode 100644 src/main/java/com/numberone/backend/domain/like/repository/ConversationLikeRepository.java diff --git a/src/main/java/com/numberone/backend/domain/conversation/controller/ConversationController.java b/src/main/java/com/numberone/backend/domain/conversation/controller/ConversationController.java new file mode 100644 index 00000000..a1cde0fe --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/controller/ConversationController.java @@ -0,0 +1,73 @@ +package com.numberone.backend.domain.conversation.controller; + +import com.numberone.backend.domain.conversation.dto.request.CreateChildConversationRequest; +import com.numberone.backend.domain.conversation.dto.request.CreateConversationRequest; +import com.numberone.backend.domain.conversation.dto.response.GetConversationResponse; +import com.numberone.backend.domain.conversation.service.ConversationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "conversations", description = "대화(재난상황 댓글) 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/conversations") +public class ConversationController { + private final ConversationService conversationService; + + @Operation(summary = "(대댓글x) 대화 생성하기", description = """ + 대화 내용을 body에 담아 전달해주세요. + + 어떤 재난상황에 대한 대화인지 재난상황 id를 body에 같이 담아 전달해주세요. + """) + @PostMapping() + public void createConversation(Authentication authentication, @RequestBody @Valid CreateConversationRequest createConversationRequest) { + conversationService.createConversation(authentication.getName(), createConversationRequest); + } + + @Operation(summary = "대댓글 대화 생성하기", description = """ + 대댓글 대화 내용을 body에 담아 전달해주세요. + + 어떤 대화(댓글)의 대댓글인지 대화 id를 파라미터로 전달해주세요. + """) + @PostMapping("/{conversationId}") + public void createChildConversation(Authentication authentication, @RequestBody @Valid CreateChildConversationRequest createConversationRequest, @PathVariable Long conversationId){ + conversationService.createChildConversation(authentication.getName(), createConversationRequest, conversationId); + } + + @Operation(summary = "(대댓글도 포함)대화 삭제하기", description = """ + 삭제할 대화 id를 파라미터로 전달해주세요. + """) + @DeleteMapping("/{conversationId}") + public void delete(@PathVariable Long conversationId) { + conversationService.delete(conversationId); + } + + @Operation(summary = "대화 조회하기", description = """ + 조회할 대화 id를 파라미터로 전달해주세요. + """) + @GetMapping("/{conversationId}") + public ResponseEntity get(Authentication authentication, @PathVariable Long conversationId) { + return ResponseEntity.ok(conversationService.get(authentication.getName(), conversationId)); + } + + @Operation(summary = "대화 좋아요 등록하기", description = """ + 사용자가 대화의 좋아요를 등록할 때 대화 id를 파라미터로 전달해주세요. + """) + @PostMapping("/like/{conversationId}") + public void increaseLike(Authentication authentication, @PathVariable Long conversationId) { + conversationService.increaseLike(authentication.getName(), conversationId); + } + + @Operation(summary = "대화 좋아요 취소하기", description = """ + 사용자가 대화의 좋아요를 취소할 때 대화 id를 파라미터로 전달해주세요. + """) + @DeleteMapping("/like/{conversationId}") + public void decreaseLike(Authentication authentication, @PathVariable Long conversationId) { + conversationService.decreaseLike(authentication.getName(), conversationId); + } +} diff --git a/src/main/java/com/numberone/backend/domain/conversation/dto/request/CreateChildConversationRequest.java b/src/main/java/com/numberone/backend/domain/conversation/dto/request/CreateChildConversationRequest.java new file mode 100644 index 00000000..0ab1177f --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/dto/request/CreateChildConversationRequest.java @@ -0,0 +1,15 @@ +package com.numberone.backend.domain.conversation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CreateChildConversationRequest { + @NotNull(message = "댓글 내용은 null일 수 없습니다.") + @Schema(defaultValue = "강남역 잠겼나요?") + private String content; +} diff --git a/src/main/java/com/numberone/backend/domain/conversation/dto/request/CreateConversationRequest.java b/src/main/java/com/numberone/backend/domain/conversation/dto/request/CreateConversationRequest.java new file mode 100644 index 00000000..9f07e657 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/dto/request/CreateConversationRequest.java @@ -0,0 +1,20 @@ +package com.numberone.backend.domain.conversation.dto.request; + + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CreateConversationRequest { + @NotNull(message = "댓글 내용은 null일 수 없습니다.") + @Schema(defaultValue = "강남역 잠겼나요?") + private String content; + + @NotNull(message = "재난 상황 id는 null일 수 없습니다.") + @Schema(defaultValue = "2") + private Long disasterId; +} diff --git a/src/main/java/com/numberone/backend/domain/conversation/dto/response/GetConversationResponse.java b/src/main/java/com/numberone/backend/domain/conversation/dto/response/GetConversationResponse.java new file mode 100644 index 00000000..0e6de71f --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/dto/response/GetConversationResponse.java @@ -0,0 +1,60 @@ +package com.numberone.backend.domain.conversation.dto.response; + +import com.numberone.backend.domain.conversation.entity.Conversation; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class GetConversationResponse { + @Schema(defaultValue = "67") + private Long conversationId; + + @Schema(defaultValue = "생존전문가 ・ 5분전") + private String info; + + @Schema(defaultValue = "강남역 잠겼나요?") + private String content; + + @Schema(defaultValue = "2") + private Long like; + + @Schema(defaultValue = "true") + private Boolean isLiked; + + @Schema(defaultValue = "true") + private Boolean isEditable; + + private List childs; + + private static String makeInfo(String nickname, LocalDateTime createdAt) { + String info = nickname + " ・ "; + Duration duration = Duration.between(createdAt, LocalDateTime.now()); + if (duration.toSeconds() < 60) + info += duration.toSeconds() + "초전"; + else if (duration.toMinutes() < 60) + info += duration.toMinutes() + "분전"; + else + info += duration.toHours() + "시간전"; + return info; + } + + public static GetConversationResponse of(Conversation conversation, Long like, Boolean isLiked, Boolean isEditable, List childs) { + return GetConversationResponse.builder() + .conversationId(conversation.getId()) + .like(like) + .info(makeInfo(conversation.getMember().getNickName(), conversation.getCreatedAt())) + .content(conversation.getContent()) + .isLiked(isLiked) + .isEditable(isEditable) + .childs(childs) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/conversation/entity/Conversation.java b/src/main/java/com/numberone/backend/domain/conversation/entity/Conversation.java new file mode 100644 index 00000000..205d8dfa --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/entity/Conversation.java @@ -0,0 +1,78 @@ +package com.numberone.backend.domain.conversation.entity; + +import com.numberone.backend.config.basetime.BaseTimeEntity; +import com.numberone.backend.domain.disaster.entity.Disaster; +import com.numberone.backend.domain.like.entity.ConversationLike; +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; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Conversation extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Comment("내용") + private String content; + + @Comment("댓글 depth {0: 댓글, 1: 대댓글}") + private Integer depth; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "disaster_id") + private Disaster disaster; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "conversation_id") + private Conversation parent; + + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) + private List conversations = new ArrayList<>(); + + @OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL) + private List likes = new ArrayList<>(); + + @Builder + public Conversation(String content, Integer likeCnt, Integer depth, Member member, Disaster disaster, Conversation parent) { + this.content = content; + this.depth = depth; + this.member = member; + this.disaster = disaster; + this.parent = parent; + } + + public static Conversation of(String content, Member member, Disaster disaster) { + return Conversation.builder() + .content(content) + .member(member) + .disaster(disaster) + .depth(0) + .likeCnt(0) + .build(); + } + + public static Conversation childOf(String content, Member member, Conversation parent) { + return Conversation.builder() + .content(content) + .member(member) + .parent(parent) + .depth(1) + .likeCnt(0) + .build(); + } + +} diff --git a/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java b/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java new file mode 100644 index 00000000..b2a807d1 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java @@ -0,0 +1,8 @@ +package com.numberone.backend.domain.conversation.repository; + + +import com.numberone.backend.domain.conversation.entity.Conversation; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ConversationRepository extends JpaRepository { +} diff --git a/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java b/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java new file mode 100644 index 00000000..10e24970 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java @@ -0,0 +1,120 @@ +package com.numberone.backend.domain.conversation.service; + +import com.numberone.backend.domain.conversation.dto.request.CreateChildConversationRequest; +import com.numberone.backend.domain.conversation.dto.request.CreateConversationRequest; +import com.numberone.backend.domain.conversation.dto.response.GetConversationResponse; +import com.numberone.backend.domain.conversation.entity.Conversation; +import com.numberone.backend.domain.conversation.repository.ConversationRepository; +import com.numberone.backend.domain.disaster.entity.Disaster; +import com.numberone.backend.domain.disaster.repository.DisasterRepository; +import com.numberone.backend.domain.like.entity.ConversationLike; +import com.numberone.backend.domain.like.repository.ConversationLikeRepository; +import com.numberone.backend.domain.member.entity.Member; +import com.numberone.backend.domain.member.service.MemberService; +import com.numberone.backend.exception.conflict.AlreadyLikedException; +import com.numberone.backend.exception.conflict.AlreadyUnLikedException; +import com.numberone.backend.exception.notfound.NotFoundConversationException; +import com.numberone.backend.exception.notfound.NotFoundDisasterException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ConversationService { + private final ConversationRepository conversationRepository; + private final MemberService memberService; + private final DisasterRepository disasterRepository; + private final ConversationLikeRepository conversationLikeRepository; + + @Transactional + public void createConversation(String email, CreateConversationRequest createConversationRequest) { + Member member = memberService.findByEmail(email); + Disaster disaster = disasterRepository.findById(createConversationRequest.getDisasterId()) + .orElseThrow(NotFoundDisasterException::new); + Conversation conversation = Conversation.of( + createConversationRequest.getContent(), + member, + disaster + ); + conversationRepository.save(conversation); + } + + @Transactional + public void createChildConversation(String email, CreateChildConversationRequest createConversationRequest, Long conversationId) { + Member member = memberService.findByEmail(email); + Conversation parent = conversationRepository.findById(conversationId) + .orElseThrow(NotFoundConversationException::new); + Conversation conversation = Conversation.childOf( + createConversationRequest.getContent(), + member, + parent + ); + conversationRepository.save(conversation); + } + + @Transactional + public void delete(Long conversationId) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(NotFoundConversationException::new); + conversationRepository.delete(conversation); + } + + private boolean checkLike(Member member, Conversation conversation) { + for (ConversationLike conversationLike : conversation.getLikes()) { + if (conversationLike.getMember().equals(member)) { + return true; + } + } + return false; + } + + public GetConversationResponse get(String email, Long conversationId) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(NotFoundConversationException::new); + Member member = memberService.findByEmail(email); + List childs = new ArrayList<>(); + for (Conversation child : conversation.getConversations()) { + childs.add(GetConversationResponse.of( + child, + conversationLikeRepository.countByConversation(child), + checkLike(member, child), + member.equals(child.getMember()), + new ArrayList<>() + )); + } + return GetConversationResponse.of( + conversation, + conversationLikeRepository.countByConversation(conversation), + checkLike(member, conversation), + member.equals(conversation.getMember()), + childs); + } + + @Transactional + public void increaseLike(String email, Long conversationId) { + Member member = memberService.findByEmail(email); + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(NotFoundConversationException::new); + conversationLikeRepository.findByMemberAndConversation(member, conversation) + .ifPresent((o) -> { + throw new AlreadyLikedException(); + }); + ConversationLike conversationLike = ConversationLike.of(member, conversation); + conversationLikeRepository.save(conversationLike); + } + + @Transactional + public void decreaseLike(String email, Long conversationId) { + Member member = memberService.findByEmail(email); + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(NotFoundConversationException::new); + ConversationLike conversationLike = conversationLikeRepository.findByMemberAndConversation(member, conversation) + .orElseThrow(AlreadyUnLikedException::new); + conversationLikeRepository.delete(conversationLike); + } +} diff --git a/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java b/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java index a83e132b..9642d74a 100644 --- a/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java +++ b/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java @@ -2,12 +2,14 @@ import com.numberone.backend.domain.disaster.dto.request.LatestDisasterRequest; import com.numberone.backend.domain.disaster.dto.response.LatestDisasterResponse; +import com.numberone.backend.domain.disaster.dto.response.SituationHomeResponse; import com.numberone.backend.domain.disaster.service.DisasterService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; @Tag(name = "disasters", description = "재난문자 관련 API") @@ -29,4 +31,12 @@ public class DisasterController { public ResponseEntity getLatestDisaster(@Valid @RequestBody LatestDisasterRequest latestDisasterRequest) { return ResponseEntity.ok(disasterService.getLatestDisaster(latestDisasterRequest)); } + + @Operation(summary = "재난상황 커뮤니티 데이터 가져오기", description = """ + 재난상황 페이지에서 필요한 재난목록과 그와 관련된 대화(댓글)들을 가져옵니다. + """) + @PostMapping("/situation") + public ResponseEntity getSituationHome(Authentication authentication){ + return ResponseEntity.ok(disasterService.getSituationHome(authentication.getName())); + } } diff --git a/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationHomeResponse.java b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationHomeResponse.java new file mode 100644 index 00000000..3ff1070a --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationHomeResponse.java @@ -0,0 +1,24 @@ +package com.numberone.backend.domain.disaster.dto.response; + +import com.numberone.backend.domain.conversation.dto.response.GetConversationResponse; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SituationHomeResponse { + List situations = new ArrayList<>(); + + public static SituationHomeResponse of(List situations) { + return SituationHomeResponse.builder() + .situations(situations) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationResponse.java b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationResponse.java new file mode 100644 index 00000000..f707880d --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationResponse.java @@ -0,0 +1,54 @@ +package com.numberone.backend.domain.disaster.dto.response; + +import com.numberone.backend.domain.conversation.dto.response.GetConversationResponse; +import com.numberone.backend.domain.disaster.entity.Disaster; +import com.numberone.backend.domain.disaster.util.DisasterType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Locale; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class SituationResponse { + @Schema(defaultValue = "32") + private Long disasterId; + + @Schema(defaultValue = "화재") + private String disasterType; + + @Schema(defaultValue = "서울특별시 강남구 동작동 화재 발생") + private String title; + + @Schema(defaultValue = "많은 비가 예상되므로 반지하주택, 지하주차장, 지하차도, 지하공간 등 침수가 우려되니 사전점검 등 산사태 위험지역 주민은 이상 징후 시 대피 바랍니다.") + private String msg; + + @Schema(defaultValue = "서울특별시 강남구 ・ 오후 2시 46분") + private String info; + + private List conversations; + + + public static SituationResponse of(Disaster disaster, List conversations) { + String category, time; + if (disaster.getDisasterType() == DisasterType.OTHERS) + category = "상황"; + else + category = disaster.getDisasterType().getDescription(); + time = disaster.getGeneratedAt().format(DateTimeFormatter.ofPattern("a h시 m분", Locale.KOREAN)); + return SituationResponse.builder() + .disasterId(disaster.getId()) + .disasterType(disaster.getDisasterType().getDescription()) + .title(disaster.getLocation() + " " + category + " 발생") + .msg(disaster.getMsg()) + .info(disaster.getLocation() + " ・ " + time) + .conversations(conversations) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/disaster/entity/Disaster.java b/src/main/java/com/numberone/backend/domain/disaster/entity/Disaster.java index a6c49c1f..80604acb 100644 --- a/src/main/java/com/numberone/backend/domain/disaster/entity/Disaster.java +++ b/src/main/java/com/numberone/backend/domain/disaster/entity/Disaster.java @@ -1,11 +1,13 @@ package com.numberone.backend.domain.disaster.entity; +import com.numberone.backend.domain.conversation.entity.Conversation; import com.numberone.backend.domain.disaster.util.DisasterType; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.Comment; import java.time.LocalDateTime; +import java.util.List; @Entity @Getter @@ -32,6 +34,9 @@ public class Disaster { @Comment("재난 발생 시각") private LocalDateTime generatedAt; + @OneToMany(mappedBy = "disaster", cascade = CascadeType.ALL) + private List conversations; + @Builder public Disaster(DisasterType disasterType, String location, String msg, Long disasterNum, LocalDateTime generatedAt) { this.disasterType = disasterType; diff --git a/src/main/java/com/numberone/backend/domain/disaster/repository/DisasterRepository.java b/src/main/java/com/numberone/backend/domain/disaster/repository/DisasterRepository.java index bf5d8a2e..8bcddecf 100644 --- a/src/main/java/com/numberone/backend/domain/disaster/repository/DisasterRepository.java +++ b/src/main/java/com/numberone/backend/domain/disaster/repository/DisasterRepository.java @@ -16,5 +16,5 @@ public interface DisasterRepository extends JpaRepository { "where :address like concat(d.location,'%') " + "and d.generatedAt > :time " + "order by d.generatedAt desc") - List findDisastersInAddressAfterTime(String address, LocalDateTime time, Pageable pageable); + List findDisastersInAddressAfterTime(String address, LocalDateTime time); } diff --git a/src/main/java/com/numberone/backend/domain/disaster/service/DisasterService.java b/src/main/java/com/numberone/backend/domain/disaster/service/DisasterService.java index 862b1241..fd6ea997 100644 --- a/src/main/java/com/numberone/backend/domain/disaster/service/DisasterService.java +++ b/src/main/java/com/numberone/backend/domain/disaster/service/DisasterService.java @@ -1,20 +1,32 @@ package com.numberone.backend.domain.disaster.service; +import com.numberone.backend.domain.conversation.dto.response.GetConversationResponse; +import com.numberone.backend.domain.conversation.entity.Conversation; +import com.numberone.backend.domain.conversation.service.ConversationService; import com.numberone.backend.domain.disaster.dto.request.LatestDisasterRequest; import com.numberone.backend.domain.disaster.dto.request.SaveDisasterRequest; import com.numberone.backend.domain.disaster.dto.response.LatestDisasterResponse; +import com.numberone.backend.domain.disaster.dto.response.SituationHomeResponse; +import com.numberone.backend.domain.disaster.dto.response.SituationResponse; import com.numberone.backend.domain.disaster.entity.Disaster; import com.numberone.backend.domain.disaster.repository.DisasterRepository; +import com.numberone.backend.domain.disaster.util.DisasterType; +import com.numberone.backend.domain.member.entity.Member; +import com.numberone.backend.domain.member.service.MemberService; +import com.numberone.backend.domain.notificationdisaster.entity.NotificationDisaster; +import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; import com.numberone.backend.util.LocationProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Service @RequiredArgsConstructor @@ -23,11 +35,13 @@ public class DisasterService { private final DisasterRepository disasterRepository; private final LocationProvider locationProvider; + private final MemberService memberService; + private final ConversationService conversationService; public LatestDisasterResponse getLatestDisaster(LatestDisasterRequest latestDisasterRequest) { String address = locationProvider.pos2address(latestDisasterRequest.getLatitude(), latestDisasterRequest.getLongitude()); LocalDateTime time = LocalDateTime.now().minusDays(1); - List disasters = disasterRepository.findDisastersInAddressAfterTime(address, time, PageRequest.of(0, 1)); + List disasters = disasterRepository.findDisastersInAddressAfterTime(address, time); if (!disasters.isEmpty()) return LatestDisasterResponse.of(disasters.get(0)); else @@ -47,4 +61,33 @@ public void save(SaveDisasterRequest saveDisasterRequest) { ); disasterRepository.save(disaster); } + + private boolean isValidDisasterType(DisasterType disasterType, List notificationDisasters) { + for (NotificationDisaster notificationDisaster : notificationDisasters) { + if (disasterType.equals(notificationDisaster.getDisasterType())) + return true; + } + return false; + } + + public SituationHomeResponse getSituationHome(String email) { + Set disasters = new HashSet<>(); + Member member = memberService.findByEmail(email); + LocalDateTime time = LocalDateTime.now().minusDays(1); + for (NotificationRegion notificationRegion : member.getNotificationRegions()) { + disasters.addAll(disasterRepository.findDisastersInAddressAfterTime(notificationRegion.getLocation(), time)); + } + disasters.removeIf(disaster -> !isValidDisasterType(disaster.getDisasterType(), member.getNotificationDisasters())); + + List situationResponses = new ArrayList<>(); + for (Disaster disaster : disasters) { + List conversationResponses = new ArrayList<>(); + for (Conversation conversation : disaster.getConversations()) { + conversationResponses.add(conversationService.get(email, conversation.getId())); + } + situationResponses.add(SituationResponse.of(disaster, conversationResponses)); + } + + return SituationHomeResponse.of(situationResponses); + } } diff --git a/src/main/java/com/numberone/backend/domain/like/entity/ConversationLike.java b/src/main/java/com/numberone/backend/domain/like/entity/ConversationLike.java new file mode 100644 index 00000000..7eddeb68 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/like/entity/ConversationLike.java @@ -0,0 +1,39 @@ +package com.numberone.backend.domain.like.entity; + +import com.numberone.backend.domain.conversation.entity.Conversation; +import com.numberone.backend.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ConversationLike { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "conversation_id") + private Conversation conversation; + + @Builder + public ConversationLike(Member member, Conversation conversation) { + this.member = member; + this.conversation = conversation; + } + + public static ConversationLike of(Member member, Conversation conversation) { + return ConversationLike.builder() + .member(member) + .conversation(conversation) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/like/repository/ConversationLikeRepository.java b/src/main/java/com/numberone/backend/domain/like/repository/ConversationLikeRepository.java new file mode 100644 index 00000000..81f0bf81 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/like/repository/ConversationLikeRepository.java @@ -0,0 +1,15 @@ +package com.numberone.backend.domain.like.repository; + +import com.numberone.backend.domain.conversation.entity.Conversation; +import com.numberone.backend.domain.like.entity.ConversationLike; +import com.numberone.backend.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; + +public interface ConversationLikeRepository extends JpaRepository { + Optional findByMemberAndConversation(Member member, Conversation conversation); + + long countByConversation(Conversation conversation); +} 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 cea2b8e5..591b3436 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 @@ -3,6 +3,7 @@ import com.numberone.backend.config.basetime.BaseTimeEntity; import com.numberone.backend.domain.like.entity.ArticleLike; import com.numberone.backend.domain.like.entity.CommentLike; +import com.numberone.backend.domain.like.entity.ConversationLike; import jakarta.persistence.*; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -63,6 +64,9 @@ public class Member extends BaseTimeEntity { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List notificationRegions = new ArrayList<>(); + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List conversationLikes = new ArrayList<>(); + @Builder public Member(String email, String nickName, String realName, Integer heartCnt, String fcmToken) { this.email = email; From 7da551afc0463bea91909310f2850c4f96f28dbd Mon Sep 17 00:00:00 2001 From: nohy6630 Date: Fri, 17 Nov 2023 14:29:38 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Feat(#55):=20=ED=99=88=20api=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=83=81=EC=9C=84=20=EC=9D=B8=EA=B8=B0=EC=88=9C=20?= =?UTF-8?q?=EB=8C=80=ED=99=94=203=EA=B0=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/GetConversationResponse.java | 4 +-- .../conversation/entity/Conversation.java | 17 +++++++++--- .../repository/ConversationRepository.java | 10 +++++++ .../service/ConversationService.java | 15 +++++++++-- .../controller/DisasterController.java | 13 +++++++++ .../dto/response/SituationDetailResponse.java | 20 ++++++++++++++ .../dto/response/SituationHomeResponse.java | 9 +++---- .../dto/response/SituationResponse.java | 6 ++++- .../disaster/service/DisasterService.java | 27 ++++++++++++++++--- 9 files changed, 104 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationDetailResponse.java diff --git a/src/main/java/com/numberone/backend/domain/conversation/dto/response/GetConversationResponse.java b/src/main/java/com/numberone/backend/domain/conversation/dto/response/GetConversationResponse.java index 0e6de71f..2ca2f387 100644 --- a/src/main/java/com/numberone/backend/domain/conversation/dto/response/GetConversationResponse.java +++ b/src/main/java/com/numberone/backend/domain/conversation/dto/response/GetConversationResponse.java @@ -46,10 +46,10 @@ else if (duration.toMinutes() < 60) return info; } - public static GetConversationResponse of(Conversation conversation, Long like, Boolean isLiked, Boolean isEditable, List childs) { + public static GetConversationResponse of(Conversation conversation, Boolean isLiked, Boolean isEditable, List childs) { return GetConversationResponse.builder() .conversationId(conversation.getId()) - .like(like) + .like(conversation.getLikeCnt()) .info(makeInfo(conversation.getMember().getNickName(), conversation.getCreatedAt())) .content(conversation.getContent()) .isLiked(isLiked) diff --git a/src/main/java/com/numberone/backend/domain/conversation/entity/Conversation.java b/src/main/java/com/numberone/backend/domain/conversation/entity/Conversation.java index 205d8dfa..df521199 100644 --- a/src/main/java/com/numberone/backend/domain/conversation/entity/Conversation.java +++ b/src/main/java/com/numberone/backend/domain/conversation/entity/Conversation.java @@ -28,6 +28,9 @@ public class Conversation extends BaseTimeEntity { @Comment("댓글 depth {0: 댓글, 1: 대댓글}") private Integer depth; + @Comment("좋아요 갯수") + private Long likeCnt; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; @@ -47,12 +50,13 @@ public class Conversation extends BaseTimeEntity { private List likes = new ArrayList<>(); @Builder - public Conversation(String content, Integer likeCnt, Integer depth, Member member, Disaster disaster, Conversation parent) { + public Conversation(String content, Long likeCnt, Integer depth, Member member, Disaster disaster, Conversation parent) { this.content = content; this.depth = depth; this.member = member; this.disaster = disaster; this.parent = parent; + this.likeCnt = likeCnt; } public static Conversation of(String content, Member member, Disaster disaster) { @@ -61,7 +65,7 @@ public static Conversation of(String content, Member member, Disaster disaster) .member(member) .disaster(disaster) .depth(0) - .likeCnt(0) + .likeCnt(0L) .build(); } @@ -71,8 +75,15 @@ public static Conversation childOf(String content, Member member, Conversation p .member(member) .parent(parent) .depth(1) - .likeCnt(0) + .likeCnt(0L) .build(); } + public void increaseLike() { + likeCnt++; + } + + public void decreaseLike() { + likeCnt--; + } } diff --git a/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java b/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java index b2a807d1..fed39925 100644 --- a/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java +++ b/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java @@ -2,7 +2,17 @@ import com.numberone.backend.domain.conversation.entity.Conversation; +import com.numberone.backend.domain.disaster.entity.Disaster; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface ConversationRepository extends JpaRepository { + + long countByDisaster(Disaster disaster); + + long countByParent(Conversation parent); + + List findAllByDisasterOrderByLikeCntDesc(Disaster disaster, Pageable pageable); } diff --git a/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java b/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java index 10e24970..7eb22c74 100644 --- a/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java +++ b/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java @@ -81,7 +81,6 @@ public GetConversationResponse get(String email, Long conversationId) { for (Conversation child : conversation.getConversations()) { childs.add(GetConversationResponse.of( child, - conversationLikeRepository.countByConversation(child), checkLike(member, child), member.equals(child.getMember()), new ArrayList<>() @@ -89,12 +88,22 @@ public GetConversationResponse get(String email, Long conversationId) { } return GetConversationResponse.of( conversation, - conversationLikeRepository.countByConversation(conversation), checkLike(member, conversation), member.equals(conversation.getMember()), childs); } + public GetConversationResponse getExceptChild(String email, Long conversationId) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(NotFoundConversationException::new); + Member member = memberService.findByEmail(email); + return GetConversationResponse.of( + conversation, + checkLike(member, conversation), + member.equals(conversation.getMember()), + new ArrayList<>()); + } + @Transactional public void increaseLike(String email, Long conversationId) { Member member = memberService.findByEmail(email); @@ -106,6 +115,7 @@ public void increaseLike(String email, Long conversationId) { }); ConversationLike conversationLike = ConversationLike.of(member, conversation); conversationLikeRepository.save(conversationLike); + conversation.increaseLike(); } @Transactional @@ -116,5 +126,6 @@ public void decreaseLike(String email, Long conversationId) { ConversationLike conversationLike = conversationLikeRepository.findByMemberAndConversation(member, conversation) .orElseThrow(AlreadyUnLikedException::new); conversationLikeRepository.delete(conversationLike); + conversation.decreaseLike(); } } diff --git a/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java b/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java index 9642d74a..7d4e894c 100644 --- a/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java +++ b/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java @@ -2,6 +2,7 @@ import com.numberone.backend.domain.disaster.dto.request.LatestDisasterRequest; import com.numberone.backend.domain.disaster.dto.response.LatestDisasterResponse; +import com.numberone.backend.domain.disaster.dto.response.SituationDetailResponse; import com.numberone.backend.domain.disaster.dto.response.SituationHomeResponse; import com.numberone.backend.domain.disaster.service.DisasterService; import io.swagger.v3.oas.annotations.Operation; @@ -39,4 +40,16 @@ public ResponseEntity getLatestDisaster(@Valid @RequestB public ResponseEntity getSituationHome(Authentication authentication){ return ResponseEntity.ok(disasterService.getSituationHome(authentication.getName())); } + + @Operation(summary = "해당 재난과 관련된 모든 커뮤니티 대화 가져오기", description = """ + 재난상황 id를 파라미터로 전달해주세요. + + 커뮤니티-재난상황-댓글더보기 페이지에서 사용하는 API입니다. + + 정렬기준을 body에 같이 전달해주세요. (최신순: time, 인기순: popularity) + """) + @PostMapping("/{disasterId}") + public ResponseEntity getSituationDetail(Authentication authentication, @PathVariable Long disasterId){ + return ResponseEntity.ok(disasterService.getSituationDetail(authentication.getName(), disasterId)); + } } diff --git a/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationDetailResponse.java b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationDetailResponse.java new file mode 100644 index 00000000..862c04aa --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationDetailResponse.java @@ -0,0 +1,20 @@ +package com.numberone.backend.domain.disaster.dto.response; + +import com.numberone.backend.domain.conversation.dto.response.GetConversationResponse; +import lombok.*; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class SituationDetailResponse { + private List conversations; + + public static SituationDetailResponse of(List conversations) { + return SituationDetailResponse.builder() + .conversations(conversations) + .build(); + } +} diff --git a/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationHomeResponse.java b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationHomeResponse.java index 3ff1070a..c66702cd 100644 --- a/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationHomeResponse.java +++ b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationHomeResponse.java @@ -1,20 +1,17 @@ package com.numberone.backend.domain.disaster.dto.response; import com.numberone.backend.domain.conversation.dto.response.GetConversationResponse; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.util.ArrayList; import java.util.List; @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder public class SituationHomeResponse { - List situations = new ArrayList<>(); + private List situations = new ArrayList<>(); public static SituationHomeResponse of(List situations) { return SituationHomeResponse.builder() diff --git a/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationResponse.java b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationResponse.java index f707880d..bf792afd 100644 --- a/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationResponse.java +++ b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationResponse.java @@ -32,10 +32,13 @@ public class SituationResponse { @Schema(defaultValue = "서울특별시 강남구 ・ 오후 2시 46분") private String info; + @Schema(defaultValue = "6") + private Long conversationCnt; + private List conversations; - public static SituationResponse of(Disaster disaster, List conversations) { + public static SituationResponse of(Disaster disaster, List conversations, Long conversationCnt) { String category, time; if (disaster.getDisasterType() == DisasterType.OTHERS) category = "상황"; @@ -49,6 +52,7 @@ public static SituationResponse of(Disaster disaster, List !isValidDisasterType(disaster.getDisasterType(), member.getNotificationDisasters())); + List situationResponses = new ArrayList<>(); for (Disaster disaster : disasters) { + Long conversationCnt=0L; List conversationResponses = new ArrayList<>(); - for (Conversation conversation : disaster.getConversations()) { - conversationResponses.add(conversationService.get(email, conversation.getId())); + conversationCnt+=conversationRepository.countByDisaster(disaster); + List conversations = conversationRepository.findAllByDisasterOrderByLikeCntDesc(disaster, PageRequest.of(0,3)); + for (Conversation conversation : conversations) { + conversationResponses.add(conversationService.getExceptChild(email, conversation.getId())); + conversationCnt+=conversationRepository.countByParent(conversation); } - situationResponses.add(SituationResponse.of(disaster, conversationResponses)); + situationResponses.add(SituationResponse.of(disaster, conversationResponses, conversationCnt)); } return SituationHomeResponse.of(situationResponses); } + + public SituationDetailResponse getSituationDetail(String email, Long disasterId) { + Disaster disaster = disasterRepository.findById(disasterId) + .orElseThrow(NotFoundDisasterException::new); + List conversationResponses = new ArrayList<>(); + for (Conversation conversation : disaster.getConversations()) { + conversationResponses.add(conversationService.get(email, conversation.getId())); + } + return SituationDetailResponse.of(conversationResponses); + } } From cc72abfa8adec4d4aa2bc3efcf6bf40e5ba238e3 Mon Sep 17 00:00:00 2001 From: nohy6630 Date: Fri, 17 Nov 2023 14:55:54 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Feat(#55):=20=EC=9D=B8=EA=B8=B0=EC=88=9C,?= =?UTF-8?q?=20=EC=B5=9C=EC=8B=A0=EC=88=9C=20=EC=A0=95=EB=A0=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=AC=EB=82=9C=EC=83=81=ED=99=A9=20=EB=8C=80=ED=99=94=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ConversationController.java | 15 ++++++++------- .../repository/ConversationRepository.java | 6 ++++++ .../conversation/service/ConversationService.java | 3 ++- .../disaster/controller/DisasterController.java | 12 +++++------- .../domain/disaster/service/DisasterService.java | 12 ++++++++++-- .../BadRequestConversationSortException.java | 11 +++++++++++ .../exception/context/CustomExceptionContext.java | 3 ++- 7 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/numberone/backend/exception/badrequest/BadRequestConversationSortException.java diff --git a/src/main/java/com/numberone/backend/domain/conversation/controller/ConversationController.java b/src/main/java/com/numberone/backend/domain/conversation/controller/ConversationController.java index a1cde0fe..3ae92f17 100644 --- a/src/main/java/com/numberone/backend/domain/conversation/controller/ConversationController.java +++ b/src/main/java/com/numberone/backend/domain/conversation/controller/ConversationController.java @@ -47,13 +47,14 @@ public void delete(@PathVariable Long conversationId) { conversationService.delete(conversationId); } - @Operation(summary = "대화 조회하기", description = """ - 조회할 대화 id를 파라미터로 전달해주세요. - """) - @GetMapping("/{conversationId}") - public ResponseEntity get(Authentication authentication, @PathVariable Long conversationId) { - return ResponseEntity.ok(conversationService.get(authentication.getName(), conversationId)); - } +// 만들었는데 필요없는 api인것 같아서 일단 주석처리 +// @Operation(summary = "대화 조회하기", description = """ +// 조회할 대화 id를 파라미터로 전달해주세요. +// """) +// @GetMapping("/{conversationId}") +// public ResponseEntity get(Authentication authentication, @PathVariable Long conversationId) { +// return ResponseEntity.ok(conversationService.get(authentication.getName(), conversationId)); +// } @Operation(summary = "대화 좋아요 등록하기", description = """ 사용자가 대화의 좋아요를 등록할 때 대화 id를 파라미터로 전달해주세요. diff --git a/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java b/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java index fed39925..a2b48fa6 100644 --- a/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java +++ b/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java @@ -15,4 +15,10 @@ public interface ConversationRepository extends JpaRepository findAllByDisasterOrderByLikeCntDesc(Disaster disaster, Pageable pageable); + + List findAllByDisasterOrderByLikeCntDesc(Disaster disaster); + + List findAllByDisasterOrderByCreatedAtDesc(Disaster disaster); + + List findAllByParentOrderByLikeCntDesc(Conversation parent); } diff --git a/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java b/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java index 7eb22c74..d59035b9 100644 --- a/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java +++ b/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java @@ -78,7 +78,8 @@ public GetConversationResponse get(String email, Long conversationId) { .orElseThrow(NotFoundConversationException::new); Member member = memberService.findByEmail(email); List childs = new ArrayList<>(); - for (Conversation child : conversation.getConversations()) { + List childConversations = conversationRepository.findAllByParentOrderByLikeCntDesc(conversation); + for (Conversation child : childConversations) { childs.add(GetConversationResponse.of( child, checkLike(member, child), diff --git a/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java b/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java index 7d4e894c..f3e18890 100644 --- a/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java +++ b/src/main/java/com/numberone/backend/domain/disaster/controller/DisasterController.java @@ -36,20 +36,18 @@ public ResponseEntity getLatestDisaster(@Valid @RequestB @Operation(summary = "재난상황 커뮤니티 데이터 가져오기", description = """ 재난상황 페이지에서 필요한 재난목록과 그와 관련된 대화(댓글)들을 가져옵니다. """) - @PostMapping("/situation") + @GetMapping("/situation") public ResponseEntity getSituationHome(Authentication authentication){ return ResponseEntity.ok(disasterService.getSituationHome(authentication.getName())); } @Operation(summary = "해당 재난과 관련된 모든 커뮤니티 대화 가져오기", description = """ - 재난상황 id를 파라미터로 전달해주세요. + 정렬기준(최신순: time, 인기순: popularity) 과 재난상황 id를 파라미터로 전달해주세요. 커뮤니티-재난상황-댓글더보기 페이지에서 사용하는 API입니다. - - 정렬기준을 body에 같이 전달해주세요. (최신순: time, 인기순: popularity) """) - @PostMapping("/{disasterId}") - public ResponseEntity getSituationDetail(Authentication authentication, @PathVariable Long disasterId){ - return ResponseEntity.ok(disasterService.getSituationDetail(authentication.getName(), disasterId)); + @GetMapping("/{sort}/{disasterId}") + public ResponseEntity getSituationDetail(Authentication authentication, @PathVariable Long disasterId, @PathVariable String sort){ + return ResponseEntity.ok(disasterService.getSituationDetail(authentication.getName(), disasterId, sort)); } } diff --git a/src/main/java/com/numberone/backend/domain/disaster/service/DisasterService.java b/src/main/java/com/numberone/backend/domain/disaster/service/DisasterService.java index 84e1a56c..a9ace5de 100644 --- a/src/main/java/com/numberone/backend/domain/disaster/service/DisasterService.java +++ b/src/main/java/com/numberone/backend/domain/disaster/service/DisasterService.java @@ -17,6 +17,7 @@ import com.numberone.backend.domain.member.service.MemberService; import com.numberone.backend.domain.notificationdisaster.entity.NotificationDisaster; import com.numberone.backend.domain.notificationregion.entity.NotificationRegion; +import com.numberone.backend.exception.badrequest.BadRequestConversationSortException; import com.numberone.backend.exception.notfound.NotFoundDisasterException; import com.numberone.backend.util.LocationProvider; import lombok.RequiredArgsConstructor; @@ -102,11 +103,18 @@ public SituationHomeResponse getSituationHome(String email) { return SituationHomeResponse.of(situationResponses); } - public SituationDetailResponse getSituationDetail(String email, Long disasterId) { + public SituationDetailResponse getSituationDetail(String email, Long disasterId, String sort) { Disaster disaster = disasterRepository.findById(disasterId) .orElseThrow(NotFoundDisasterException::new); List conversationResponses = new ArrayList<>(); - for (Conversation conversation : disaster.getConversations()) { + List conversations; + if(sort.equals("popularity")) + conversations = conversationRepository.findAllByDisasterOrderByLikeCntDesc(disaster); + else if(sort.equals("time")) + conversations = conversationRepository.findAllByDisasterOrderByCreatedAtDesc(disaster); + else + throw new BadRequestConversationSortException(); + for (Conversation conversation : conversations) { conversationResponses.add(conversationService.get(email, conversation.getId())); } return SituationDetailResponse.of(conversationResponses); diff --git a/src/main/java/com/numberone/backend/exception/badrequest/BadRequestConversationSortException.java b/src/main/java/com/numberone/backend/exception/badrequest/BadRequestConversationSortException.java new file mode 100644 index 00000000..e86bba4b --- /dev/null +++ b/src/main/java/com/numberone/backend/exception/badrequest/BadRequestConversationSortException.java @@ -0,0 +1,11 @@ +package com.numberone.backend.exception.badrequest; + +import com.numberone.backend.exception.context.ExceptionContext; + +import static com.numberone.backend.exception.context.CustomExceptionContext.BAD_REQUEST_CONVERSATION_SORT; + +public class BadRequestConversationSortException extends BadRequestException{ + public BadRequestConversationSortException() { + super(BAD_REQUEST_CONVERSATION_SORT); + } +} 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 d0988aa2..ca8601b0 100644 --- a/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java +++ b/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java @@ -53,7 +53,8 @@ public enum CustomExceptionContext implements ExceptionContext { ALREADY_UNLIKED_ERROR("이미 좋아요 해제 처리된 엔티티입니다.", 11001), //conversation 관련 예외 - NOT_FOUND_CONVERSATION("해당 대화를 찾을 수 없습니다.", 12000) + NOT_FOUND_CONVERSATION("해당 대화를 찾을 수 없습니다.", 12000), + BAD_REQUEST_CONVERSATION_SORT("정렬 기준 값을 올바르게 전달해주세요. (popularity 또는 time)",12001) ; private final String message; From 2b297f3d97d58fbebab6c7310a411f21d9aaf682 Mon Sep 17 00:00:00 2001 From: JaeHyeon Lee Date: Fri, 17 Nov 2023 18:32:15 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Feat:=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B4=80=EB=A0=A8=20API=20=EC=9A=94=EA=B5=AC?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat(#54): resolved rebase conflicts Feat(#54): 커뮤니티 API 기초 구현 Feat(#54): 게시글 상세 조회 시 태그 포함 Feat(#54): todo 작성 Feat(#54): 게시글 생성 시 이미지 업로드 bug fix Feat(#54): 게시글 상세 조회 시 좋아요 여부와 작성자 프로필 사진 url 반환 Feat(#54): 게시글 상세 조회 시 좋아요 여부와 작성자 프로필 사진 url 반환 Feat(#54): 게시글 리스트 조회 시, 좋아요 누른 여부를 포함 Feat(#54): todo 주석 제거 및 더미데이터 제거 Feat(#54): 썸네일 이미지 관련 요구사항 적용 - 첫 이미지를 썸네일 이미지로 지정 Feat(#54): 게시물 카테고리 수정 - 일상, 교통, 치안, 기타 Hotfix: npe fix Feat(#54): rename field for resolve rebase conflict Docs(#54): update todo Fix(#54): 좋아요 버그 수정 Fix(#54): 스웨거 jwt 버그 수정 Feat(#54): 게시글/댓글 좋아요 api 구현 Feat(#54): 알림 저장 시 닉네임도 저장 Feat(#54): 좋아요 관련 예외 로직 Feat(#54): 푸시 알림 태그 구분 Feat(#54): 푸시 알림 발송 시 db 에 저장 Feat(#54): 게시글 좋아요 api (wip) Feat(#54): 게시글 좋아요 api (wip) Feat(#54): improvement community apis - 게시글 작성 시, 작성자의 주소 로깅 - 게시글에 댓글 등록 시 푸시 알림 전송 Feat(#54): improvement community apis - 게시글 작성 시, 작성자의 주소 로깅 - 게시글에 댓글 등록 시 푸시 알림 전송 Feat(#54): catch exception Docs(#54): todo 작성 Feat(#54): 게시글 수정하기 api 구현 및 게시글 삭제 http method 를 patch 로 변경 Feat(#54): 대댓글 계층형 조회 api 구현 Feat(#54): 대댓글 작성 api 구현 Feat(#54): 게시글에 댓글 작성하기 api 구현 Feat(#54): 게시글 조회 no offset paging api 구현 Feat(#54): removed test api for fcm Feat(#54): removed test api for fcm Feat(#54): 게시글 상세 조회 시, 제목 및 내용 포함 Feat(#54): 게시글 상세 조회 및 삭제 API 구현 Docs(#54): 게시글 작성 API Feat(#54): removed Qclass Feat(#54): 게시글 활성화 상태 필드 추가 Feat(#54): 게시글 작성 API 구현 Feat(#54): rename CommunityParticipant - to ArticleParticipant Feat(#54): 게시글, 댓글, 커뮤니티 참여자 레포지토리 구현 Feat(#54): relocate s3Provider Docs(#54): 프로필 사진 업로드 api - swagger api summary, description Feat(#54): 커뮤니티 엔티티 설계 - Article: 동네생활 게시글 - ArticleTag: 게시글 태그 - CommentEntity: 댓글 - CommunityParticipant: 게시글 참여자 Feat(#54): 회원 프로필 사진 업로드 API 구현 Feat(#54): 회원 엔티티 필드 추가 - 프로필 사진, 닉네임, 실명, fcm 토큰 Feat(#54): implement SecurityContextProvider - for parsing principal from jwt token Feat(#54): member extends baseTimeEntity * Feat(#54): fix invalid uri prefix * Feat(#54): fix invalid uri prefix * Feat(#54): 댓글 리스트 조회 시, 좋아요 여부 반환하도록 변경 * Feat(#54): 댓글 삭제 api 구현 * Feat(#54): 댓글 삭제 api docs 작성 * Feat(#54): comment count 관련 bug fix * Feat(#54): removed unused field * Feat(#54): 베스트 게시글 작성자에게 푸시알람 전송 * Feat(#54): 베스트 게시글 작성자에게 푸시알람 전송 --- .../article/controller/ArticleController.java | 8 ++-- .../dto/request/UploadArticleRequest.java | 1 - .../response/GetArticleDetailResponse.java | 32 +++++++++++---- .../dto/response/GetArticleListResponse.java | 17 ++++++-- .../dto/response/ModifyArticleResponse.java | 4 +- .../dto/response/UploadArticleResponse.java | 2 +- .../domain/article/entity/Article.java | 7 +--- .../domain/article/entity/ArticleTag.java | 4 +- .../article/service/ArticleService.java | 39 ++++++++++++++----- .../comment/controller/CommentController.java | 18 ++++++++- .../dto/response/DeleteCommentResponse.java | 12 ++++++ .../comment/dto/response/GetCommentDto.java | 4 +- .../domain/comment/entity/CommentEntity.java | 2 +- .../custom/CommentRepositoryCustom.java | 3 +- .../custom/CommentRepositoryCustomImpl.java | 11 ++++++ .../comment/service/CommentService.java | 23 ++++++++++- .../domain/like/service/LikeService.java | 16 +++++++- .../member/controller/MemberController.java | 2 +- .../backend/domain/member/entity/Member.java | 1 + .../domain/member/service/MemberService.java | 2 +- .../fcm/service/FcmMessageProvider.java | 1 + .../notification/NotificationMessage.java | 4 +- 22 files changed, 165 insertions(+), 48 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.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 9bcb9151..d4bea002 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 @@ -34,16 +34,16 @@ public class ArticleController { 1. title 은 글 제목 입니다 (not null) 2. content 는 글 내용 입니다 (not null) - 3. articleTag 는 게시글 태그 입니다. LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보) + 3. articleTag 는 게시글 태그 입니다. LIFE(일상), TRAFFIC(교통), SAFETY(치안), NONE(기타) -> 영어로 보내주세요 4. imageList 는 이미지 (MultiPart) 리스트 입니다. - 5. thumbNailImageIdx 는 썸네일 이미지의 인덱스 입니다. (0,1,2, ... + 5. imageList 의 첫 원소를 썸네일로 지정합니다. imageList 에 이미지를 담아서 보내는 경우, idx 에 따라서 썸네일 이미지를 결정합니다. """) @PostMapping - public ResponseEntity uploadArticle(@RequestBody @Valid UploadArticleRequest request) { + public ResponseEntity uploadArticle(@ModelAttribute @Valid UploadArticleRequest request) { return ResponseEntity.created(URI.create("/api/articles")) .body(articleService.uploadArticle(request)); } @@ -87,7 +87,7 @@ public ResponseEntity getArticleDetails(@PathVariable( @GetMapping public ResponseEntity> getArticlePages( Pageable pageable, - @ModelAttribute ArticleSearchParameter param) { // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정 + @ModelAttribute ArticleSearchParameter param) { return ResponseEntity.ok(articleService.getArticleListPaging(param, pageable)); } 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 04b76643..9bcea7c0 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 @@ -30,7 +30,6 @@ 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/GetArticleDetailResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java index cc356667..a695eb61 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 @@ -1,6 +1,8 @@ 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.member.entity.Member; import lombok.*; @@ -22,18 +24,28 @@ public class GetArticleDetailResponse { private LocalDateTime modifiedAt; private String title; private String content; + private boolean isLiked; + private ArticleTag articleTag; + private Long commentCount; // 작성자 관련 - private String memberName; - private String memberNickName; - private String address; // todo: 더미 데이터 + private String ownerName; + private String ownerNickName; + private String address; private Long ownerMemberId; + private String ownerProfileImageUrl; // 이미지 관련 private List imageUrls; private String thumbNailImageUrl; - public static GetArticleDetailResponse of(Article article, List imageUrls, String thumbNailImageUrl, Member member){ + public static GetArticleDetailResponse of( + Article article, + List imageUrls, + String thumbNailImageUrl, + Member owner, + List memberLikedArticleList, + Long commentCount ) { return GetArticleDetailResponse.builder() .articleId(article.getId()) .title(article.getTitle()) @@ -45,12 +57,16 @@ public static GetArticleDetailResponse of(Article article, List imageUrl ) .createdAt(article.getCreatedAt()) .modifiedAt(article.getModifiedAt()) - .ownerMemberId(member.getId()) - .memberName(member.getRealName()) - .memberNickName(member.getNickName()) + .ownerMemberId(owner.getId()) + .ownerName(owner.getRealName()) + .ownerNickName(owner.getNickName()) .imageUrls(imageUrls) .thumbNailImageUrl(thumbNailImageUrl) - .address("서울시 광진구 자양동") // 교체 + .address(article.getAddress()) + .ownerProfileImageUrl(owner.getProfileImageUrl()) + .isLiked(memberLikedArticleList.contains(article.getId())) + .articleTag(article.getArticleTag()) + .commentCount(commentCount) .build(); } 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 index 6b88af6d..cf86ba21 100644 --- 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 @@ -9,6 +9,7 @@ import lombok.*; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; @ToString @@ -31,7 +32,8 @@ public class GetArticleListResponse { private Long thumbNailImageId; private Integer articleLikeCount; - private Integer commentCount; + private Long commentCount; + private Boolean isLiked; @QueryProjection @@ -46,7 +48,6 @@ public GetArticleListResponse(Article article, Long ownerId, Long thumbNailImage this.articleStatus = article.getArticleStatus(); this.thumbNailImageId = thumbNailImageId; this.articleLikeCount = article.getLikeCount(); - this.commentCount = article.getCommentCount(); } public void setOwnerNickName(String nickName){ @@ -57,7 +58,14 @@ public void setThumbNailImageUrl(String thumbNailImageUrl){ this.thumbNailImageUrl = thumbNailImageUrl; } - public void updateInfo(Optional owner, Optional articleImage){ + public void setCommentCount(Long commentCount){ + this.commentCount = commentCount; + } + + public void updateInfo(Optional owner, + Optional articleImage, + List memberLikedArticleIdList, + Long commentCount ){ owner.ifPresentOrElse( o -> setOwnerNickName(o.getNickName()), () -> setOwnerNickName("알 수 없는 사용자") @@ -66,7 +74,8 @@ public void updateInfo(Optional owner, Optional articleIma image -> setThumbNailImageUrl(image.getImageUrl()), () -> setThumbNailImageUrl("") ); + this.isLiked = memberLikedArticleIdList.contains(id); + this.commentCount = commentCount; } - } 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 index 88a57536..62ce0b63 100644 --- 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 @@ -25,7 +25,7 @@ public class ModifyArticleResponse { private String thumbNailImageUrl; // 작성자 주소 - private String address; // todo: 더미 데이터 + private String address; public static ModifyArticleResponse of(Article article, List imageUrls, String thumbNailImageUrl){ return ModifyArticleResponse.builder() @@ -34,7 +34,7 @@ public static ModifyArticleResponse of(Article article, List imageUrls, .modifiedAt(article.getModifiedAt()) .imageUrls(imageUrls) .thumbNailImageUrl(thumbNailImageUrl) - .address("서울시 광진구 자양동") + .address(article.getAddress()) .build(); } 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 fef61aa5..c18a13c7 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 @@ -21,7 +21,7 @@ public class UploadArticleResponse { private String thumbNailImageUrl; // 작성자 주소 - private String address; // todo: 더미 데이터 + private String address; public static UploadArticleResponse of(Article article, List imageUrls, String thumbNailImageUrl){ return UploadArticleResponse.builder() 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 c0bea662..92f0785f 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 @@ -56,11 +56,7 @@ public class Article extends BaseTimeEntity { @ColumnDefault("0") @Comment("게시글 좋아요 개수") - private Integer likeCount; // todo: 동시성 처리 - - @ColumnDefault("0") - @Comment("게시글에 달린 댓글 개수") - private Integer commentCount; + private Integer likeCount; @Comment("작성자 ID") private Long articleOwnerId; @@ -71,7 +67,6 @@ public Article(String title, String content, Long articleOwnerId, ArticleTag tag this.articleOwnerId = articleOwnerId; this.articleTag = tag; this.articleStatus = ArticleStatus.ACTIVATED; - this.commentCount = 0; this.likeCount = 0; } 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 index 1ebf82a4..f5215017 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/ArticleTag.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/ArticleTag.java @@ -5,8 +5,8 @@ @RequiredArgsConstructor public enum ArticleTag { LIFE, // 일상 - FRAUD, // 사기 + TRAFFIC, // 교통 SAFETY, // 치안 - REPORT; // 제보 + NONE; // 기타 private String value; } 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 c2c5d312..52de3c0d 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 @@ -14,6 +14,8 @@ 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.like.entity.ArticleLike; +import com.numberone.backend.domain.like.repository.ArticleLikeRepository; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.notification.entity.NotificationTag; @@ -50,6 +52,7 @@ public class ArticleService { private final ArticleParticipantRepository articleParticipantRepository; private final ArticleImageRepository articleImageRepository; private final CommentRepository commentRepository; + private final ArticleLikeRepository articleLikeRepository; private final S3Provider s3Provider; private final LocationProvider locationProvider; private final FcmMessageProvider fcmMessageProvider; @@ -68,6 +71,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { owner.getId(), request.getArticleTag()) ); + articleParticipantRepository.save( new ArticleParticipant(article, owner) ); @@ -89,7 +93,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { new ArticleImage(article, imageUrl) ); articleImages.add(savedArticleImage); - if (Objects.equals(i, request.getThumbNailImageIdx())) { + if (i == 0) { thumbNailImageUrl = imageUrl; thumbNailImageId = savedArticleImage.getId(); } @@ -104,6 +108,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { Double latitude = request.getLatitude(); Double longitude = request.getLongitude(); if (latitude != null && longitude != null) { + // 주소가 null 이 아닌 경우에만 api 요청하여 update String address = locationProvider.pos2address(request.getLatitude(), request.getLongitude()); article.updateAddress(address); } @@ -122,10 +127,12 @@ public DeleteArticleResponse deleteArticle(Long articleId) { public GetArticleDetailResponse getArticleDetail(Long articleId) { String principal = SecurityContextProvider.getAuthenticatedUserEmail(); - Member owner = memberRepository.findByEmail(principal) + Member member = memberRepository.findByEmail(principal) // 회원 .orElseThrow(NotFoundMemberException::new); Article article = articleRepository.findById(articleId) .orElseThrow(NotFoundArticleException::new); + Member owner = memberRepository.findById(article.getArticleOwnerId()) // 작성자 + .orElseThrow(NotFoundMemberException::new); List imageUrls = articleImageRepository.findByArticle(article) .stream() @@ -134,32 +141,46 @@ public GetArticleDetailResponse getArticleDetail(Long articleId) { Optional thumbNailImage = articleImageRepository.findById(article.getThumbNailImageUrlId()); + Long commentCount = commentRepository.countAllByArticle(articleId); String thumbNailImageUrl = ""; if (thumbNailImage.isPresent()) { thumbNailImageUrl = thumbNailImage.get().getImageUrl(); } - return GetArticleDetailResponse.of(article, imageUrls, thumbNailImageUrl, owner); + // 내가 좋아요 한 게시글의 ID 리스트 + List memberLikedArticleIdList = articleLikeRepository.findByMember(member) + .stream().map(ArticleLike::getArticleId) + .toList(); + + return GetArticleDetailResponse.of(article, imageUrls, thumbNailImageUrl, owner, memberLikedArticleIdList, commentCount); } public Slice getArticleListPaging(ArticleSearchParameter param, Pageable pageable) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + List memberLikedArticleIdList = articleLikeRepository.findByMember(member) + .stream().map(ArticleLike::getArticleId) + .toList(); return new SliceImpl<>( articleRepository.getArticlesNoOffSetPaging(param, pageable) .stream() - .peek(this::updateArticleInfo) - .toList() - ); + .peek(article -> { + updateArticleInfo(article, memberLikedArticleIdList); + }) + .toList()); } - public void updateArticleInfo(GetArticleListResponse articleInfo) { + public void updateArticleInfo(GetArticleListResponse articleInfo, List memberLikedArticleIdList) { Long ownerId = articleInfo.getOwnerId(); Long thumbNailImageUrlId = articleInfo.getThumbNailImageId(); Optional owner = memberRepository.findById(ownerId); Optional articleImage = articleImageRepository.findById(thumbNailImageUrlId); + Long commentCount = commentRepository.countAllByArticle(articleInfo.getId()); - articleInfo.updateInfo(owner, articleImage); + articleInfo.updateInfo(owner, articleImage, memberLikedArticleIdList, commentCount); } @Transactional @@ -174,8 +195,8 @@ public CreateCommentResponse createComment(Long articleId, CreateCommentRequest ); articleParticipantRepository.save(new ArticleParticipant(article, member)); + // 게시글 작성자에게 알림을 보낸다. fcmMessageProvider.sendFcm(member, ARTICLE_COMMENT_FCM_ALARM, NotificationTag.COMMUNITY); - return CreateCommentResponse.of(savedComment); } 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 6ccf4158..be571887 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,12 +2,14 @@ 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.DeleteCommentResponse; 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; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.hibernate.sql.Delete; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -56,10 +58,22 @@ public ResponseEntity createChildComment( """) @GetMapping("{article-id}") public ResponseEntity> getCommentsByArticle(@PathVariable("article-id") Long articleId){ - List response = commentService.getCommentsByArticle(articleId); // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정 + List response = commentService.getCommentsByArticle(articleId); return ResponseEntity.ok(response); } - // todo: 댓글 삭제, 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능 + @Operation(summary = "댓글 삭제 API 입니다", description = """ + 삭제할 댓글의 id 를 path variable 으로 보내주세요. + + 대댓글이 존재하는 댓글을 삭제 요청하는 경우에는, 대댓글까지 모두 삭제됩니다. + + 대댓글이 없는 댓글을 삭제 요청하는 경우에는 해당 댓글만 삭제됩니다. + """) + @DeleteMapping("{comment-id}") + public ResponseEntity deleteComment(@PathVariable("comment-id") Long commentId){ + return ResponseEntity.ok(commentService.deleteComment(commentId)); + } + + // todo: 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능 } diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.java b/src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.java new file mode 100644 index 00000000..7c06b632 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.java @@ -0,0 +1,12 @@ +package com.numberone.backend.domain.comment.dto.response; + +import lombok.*; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class DeleteCommentResponse { + private Long commentId; +} 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 index 4fe99af3..efedf0c3 100644 --- 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 @@ -32,6 +32,7 @@ public class GetCommentDto { private Long authorId; private String authorNickName; private String authorProfileImageUrl; + private boolean isLiked; @QueryProjection @@ -47,7 +48,7 @@ public GetCommentDto(CommentEntity comment){ this.likeCount = comment.getLikeCount(); } - public void updateCommentInfo(Optional author){ + public void updateCommentInfo(Optional author, List likedCommentIdList){ author.ifPresentOrElse( a -> { this.authorNickName = a.getNickName(); @@ -57,6 +58,7 @@ public void updateCommentInfo(Optional author){ this.authorNickName = "알 수 없는 사용자"; } ); + this.isLiked = likedCommentIdList.contains(commentId); } } 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 91fb1299..24978a86 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 @@ -31,7 +31,7 @@ public class CommentEntity extends BaseTimeEntity { private Integer depth; @Comment("댓글 좋아요 개수") - private Integer likeCount; // todo: 동시성 처리 + private Integer likeCount; @Comment("댓글 내용") private String content; 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 index fe326c39..66adfcbf 100644 --- 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 @@ -5,6 +5,7 @@ import java.util.List; public interface CommentRepositoryCustom { - public List findAllByArticle(Long articleId); + List findAllByArticle(Long articleId); + Long countAllByArticle(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 index 69331895..ce15479f 100644 --- 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 @@ -31,4 +31,15 @@ public List findAllByArticle(Long articleId) { ) .fetch(); } + + + @Override + public Long countAllByArticle(Long articleId) { + return queryFactory.select(commentEntity.count()) + .from(commentEntity) + .innerJoin(article, article) + .where(article.id.eq(articleId)) + .fetchOne(); + + } } 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 ed8b83d7..825ab152 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,9 +6,12 @@ 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.DeleteCommentResponse; 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.like.entity.CommentLike; +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; @@ -32,6 +35,7 @@ public class CommentService { private final ArticleRepository articleRepository; private final ArticleParticipantRepository articleParticipantRepository; private final MemberRepository memberRepository; + private final CommentLikeRepository commentLikeRepository; @Transactional public CreateChildCommentResponse createChildComment( @@ -56,10 +60,15 @@ public CreateChildCommentResponse createChildComment( } public List getCommentsByArticle(Long articleId) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); Article article = articleRepository.findById(articleId) .orElseThrow(NotFoundArticleException::new); List comments = commentRepository.findAllByArticle(article.getId()); - + List likedCommentIdList = commentLikeRepository.findByMember(member) + .stream().map(CommentLike::getCommentId) + .toList(); // 계층 구조로 변환 (추후 리팩토링 필요) List result = new ArrayList<>(); Map map = new HashMap<>(); @@ -68,7 +77,7 @@ public List getCommentsByArticle(Long articleId) { CommentEntity commentEntity = commentRepository.findById(comment.getCommentId()) .orElseThrow(NotFoundCommentException::new); Optional author = memberRepository.findById(commentEntity.getAuthorId()); - comment.updateCommentInfo(author); + comment.updateCommentInfo(author, likedCommentIdList); map.put(comment.getCommentId(), comment); @@ -85,4 +94,14 @@ public List getCommentsByArticle(Long articleId) { return result; } + @Transactional + public DeleteCommentResponse deleteComment(Long commentId){ + CommentEntity commentEntity = commentRepository.findById(commentId) + .orElseThrow(NotFoundCommentException::new); + commentRepository.delete(commentEntity); + return DeleteCommentResponse.builder() + .commentId(commentId) + .build(); + } + } 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 c43c90d7..f215355e 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 @@ -10,19 +10,24 @@ 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.notification.entity.NotificationTag; 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; +import com.numberone.backend.support.fcm.service.FcmMessageProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.hibernate.id.IntegralDataTypeHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import static com.numberone.backend.support.notification.NotificationMessage.BEST_ARTICLE_FCM_ALARM; + @Slf4j @Service @RequiredArgsConstructor @@ -34,7 +39,9 @@ public class LikeService { private final CommentLikeRepository commentLikeRepository; private final CommentRepository commentRepository; private final MemberRepository memberRepository; + private final FcmMessageProvider fcmMessageProvider; + private final Integer BEST_ARTICLE_LIKE_COUNT = 20; @Transactional public Integer increaseArticleLike(Long articleId) { @@ -50,6 +57,13 @@ public Integer increaseArticleLike(Long articleId) { article.increaseLikeCount(); articleLikeRepository.save(new ArticleLike(member, article)); + if (article.getLikeCount() >= BEST_ARTICLE_LIKE_COUNT) { + Long ownerId = article.getArticleOwnerId(); + Member owner = memberRepository.findById(ownerId) + .orElseThrow(NotFoundMemberException::new); + fcmMessageProvider.sendFcm(owner, BEST_ARTICLE_FCM_ALARM, NotificationTag.COMMUNITY); + } + return article.getLikeCount(); } @@ -97,7 +111,7 @@ public Integer decreaseCommentLike(Long commentId) { .orElseThrow(NotFoundMemberException::new); CommentEntity commentEntity = commentRepository.findById(commentId) .orElseThrow(NotFoundCommentException::new); - if (!isAlreadyLikedComment(member, commentId)){ + if (!isAlreadyLikedComment(member, commentId)) { // 좋아요를 누르지 않은 댓글이라 좋아요를 취소할 수 없습니다. throw new AlreadyUnLikedException(); } 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 6acfc70b..2344e859 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 @@ -28,8 +28,8 @@ @Slf4j @Tag(name = "members", description = "사용자 관련 API") @RestController -@RequiredArgsConstructor @RequestMapping("/api/members") +@RequiredArgsConstructor public class MemberController { private final MemberService memberService; 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 cea2b8e5..53d9ee4a 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 @@ -16,6 +16,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; +import org.springframework.web.bind.annotation.RequestMapping; import java.util.ArrayList; import java.util.List; 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 cab4b378..7107ebc6 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 @@ -46,8 +46,8 @@ public void create(String email, String realName) { public void initMemberData(String email, OnboardingRequest onboardingRequest) { Member member = memberRepository.findByEmail(email) .orElseThrow(NotFoundMemberException::new); - member.setOnboardingData(onboardingRequest.getNickname(), onboardingRequest.getFcmToken()); notificationDisasterRepository.deleteAllByMemberId(member.getId()); + member.setOnboardingData(onboardingRequest.getNickname(), onboardingRequest.getFcmToken()); notificationRegionRepository.deleteAllByMemberId(member.getId()); for (OnboardingAddress address : onboardingRequest.getAddresses()) { notificationRegionRepository.save(NotificationRegion.of( 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 5bfff921..62131f2e 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 @@ -27,6 +27,7 @@ public void sendFcm(Member member, NotificationMessage notificationMessage, Noti String token = member.getFcmToken(); if (Objects.isNull(token)){ log.error("해당 회원의 fcm 토큰이 존재하지 않아, 푸시알람을 전송할 수 없습니다."); + // todo : 예외 핸들링 return; } diff --git a/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java b/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java index 8fdf8316..aa0a387c 100644 --- a/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java +++ b/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java @@ -7,7 +7,9 @@ @RequiredArgsConstructor public enum NotificationMessage implements NotificationMessageSpec { - ARTICLE_COMMENT_FCM_ALARM("[대피로 알림]", "게시글에 댓글이 달렸어요!", null); + ARTICLE_COMMENT_FCM_ALARM("[대피로 알림]", "게시글에 댓글이 달렸어요!", null), + BEST_ARTICLE_FCM_ALARM("[대피로 알림]", "축하드립니다! 베스트 게시글로 선정되었습니다. 🎉", null); + ; private final String title; private final String body;