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..3ae92f17 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/controller/ConversationController.java @@ -0,0 +1,74 @@ +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); + } + +// 만들었는데 필요없는 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를 파라미터로 전달해주세요. + """) + @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..2ca2f387 --- /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, Boolean isLiked, Boolean isEditable, List childs) { + return GetConversationResponse.builder() + .conversationId(conversation.getId()) + .like(conversation.getLikeCnt()) + .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..df521199 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/entity/Conversation.java @@ -0,0 +1,89 @@ +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; + + @Comment("좋아요 갯수") + private Long likeCnt; + + @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, 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) { + return Conversation.builder() + .content(content) + .member(member) + .disaster(disaster) + .depth(0) + .likeCnt(0L) + .build(); + } + + public static Conversation childOf(String content, Member member, Conversation parent) { + return Conversation.builder() + .content(content) + .member(member) + .parent(parent) + .depth(1) + .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 new file mode 100644 index 00000000..a2b48fa6 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/repository/ConversationRepository.java @@ -0,0 +1,24 @@ +package com.numberone.backend.domain.conversation.repository; + + +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); + + 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 new file mode 100644 index 00000000..d59035b9 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/conversation/service/ConversationService.java @@ -0,0 +1,132 @@ +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<>(); + List childConversations = conversationRepository.findAllByParentOrderByLikeCntDesc(conversation); + for (Conversation child : childConversations) { + childs.add(GetConversationResponse.of( + child, + checkLike(member, child), + member.equals(child.getMember()), + new ArrayList<>() + )); + } + return GetConversationResponse.of( + 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); + 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); + conversation.increaseLike(); + } + + @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); + 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 a83e132b..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 @@ -2,12 +2,15 @@ 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; 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 +32,22 @@ public class DisasterController { public ResponseEntity getLatestDisaster(@Valid @RequestBody LatestDisasterRequest latestDisasterRequest) { return ResponseEntity.ok(disasterService.getLatestDisaster(latestDisasterRequest)); } + + @Operation(summary = "재난상황 커뮤니티 데이터 가져오기", description = """ + 재난상황 페이지에서 필요한 재난목록과 그와 관련된 대화(댓글)들을 가져옵니다. + """) + @GetMapping("/situation") + public ResponseEntity getSituationHome(Authentication authentication){ + return ResponseEntity.ok(disasterService.getSituationHome(authentication.getName())); + } + + @Operation(summary = "해당 재난과 관련된 모든 커뮤니티 대화 가져오기", description = """ + 정렬기준(최신순: time, 인기순: popularity) 과 재난상황 id를 파라미터로 전달해주세요. + + 커뮤니티-재난상황-댓글더보기 페이지에서 사용하는 API입니다. + """) + @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/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 new file mode 100644 index 00000000..c66702cd --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationHomeResponse.java @@ -0,0 +1,21 @@ +package com.numberone.backend.domain.disaster.dto.response; + +import com.numberone.backend.domain.conversation.dto.response.GetConversationResponse; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class SituationHomeResponse { + private 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..bf792afd --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/disaster/dto/response/SituationResponse.java @@ -0,0 +1,58 @@ +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; + + @Schema(defaultValue = "6") + private Long conversationCnt; + + private List conversations; + + + public static SituationResponse of(Disaster disaster, List conversations, Long conversationCnt) { + 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) + .conversationCnt(conversationCnt) + .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..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 @@ -1,10 +1,24 @@ 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.repository.ConversationRepository; +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.SituationDetailResponse; +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.exception.badrequest.BadRequestConversationSortException; +import com.numberone.backend.exception.notfound.NotFoundDisasterException; import com.numberone.backend.util.LocationProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,9 +26,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.awt.print.Pageable; 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 +41,14 @@ public class DisasterService { private final DisasterRepository disasterRepository; private final LocationProvider locationProvider; + private final MemberService memberService; + private final ConversationService conversationService; + private final ConversationRepository conversationRepository; 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 +68,55 @@ 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) { + Long conversationCnt=0L; + List conversationResponses = new ArrayList<>(); + 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, conversationCnt)); + } + + return SituationHomeResponse.of(situationResponses); + } + + public SituationDetailResponse getSituationDetail(String email, Long disasterId, String sort) { + Disaster disaster = disasterRepository.findById(disasterId) + .orElseThrow(NotFoundDisasterException::new); + List conversationResponses = new ArrayList<>(); + 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/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 53d9ee4a..67de046c 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; @@ -64,6 +65,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; 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 13eca97d..ca8601b0 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,10 @@ public enum CustomExceptionContext implements ExceptionContext { // like 관련 예외 ALREADY_LIKED_ERROR("이미 좋아요 처리된 엔티티입니다.", 11000), ALREADY_UNLIKED_ERROR("이미 좋아요 해제 처리된 엔티티입니다.", 11001), + + //conversation 관련 예외 + NOT_FOUND_CONVERSATION("해당 대화를 찾을 수 없습니다.", 12000), + BAD_REQUEST_CONVERSATION_SORT("정렬 기준 값을 올바르게 전달해주세요. (popularity 또는 time)",12001) ; 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); + } +}