diff --git a/config b/config index d72c959b..fa7a7cb1 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit d72c959bf18f2ae3886ed406708a23128e1f7696 +Subproject commit fa7a7cb1581f3ff89918a6f2f07e354aa96d95a9 diff --git a/src/main/java/com/koliving/api/KolivingApplication.java b/src/main/java/com/koliving/api/KolivingApplication.java index 9896fcd3..79b2ce8d 100644 --- a/src/main/java/com/koliving/api/KolivingApplication.java +++ b/src/main/java/com/koliving/api/KolivingApplication.java @@ -12,6 +12,8 @@ import com.koliving.api.i18n.LanguageRepository; import com.koliving.api.location.domain.Location; import com.koliving.api.location.infra.LocationRepository; +import com.koliving.api.report.application.ReportReasonRepository; +import com.koliving.api.report.domain.ReportReason; import com.koliving.api.room.domain.Furnishing; import com.koliving.api.room.domain.FurnishingType; import com.koliving.api.room.domain.Like; @@ -71,7 +73,8 @@ CommandLineRunner commandLineRunner( LikeRepository likeRepository, UserRepository userRepository, PasswordEncoder encoder, - NotificationRepository notificationRepository + NotificationRepository notificationRepository, + ReportReasonRepository reportReasonRepository ) { return args -> { initImageFiles(imageFileRepository); @@ -80,9 +83,21 @@ CommandLineRunner commandLineRunner( initLanguages(languageRepository); User user = initUser(userRepository, encoder, notificationRepository); initRooms(roomRepository, locationRepository, furnishingRepository, imageFileRepository, likeRepository, user); + initReport(reportReasonRepository); }; } + private void initReport(ReportReasonRepository reportReasonRepository) { + reportReasonRepository.saveAll( + List.of( + ReportReason.of("Not a real Place"), + ReportReason.of("Inappropriate content"), + ReportReason.of("Incorrect information"), + ReportReason.of("Suspected scammer") + ) + ); + } + private User initUser(UserRepository userRepository, PasswordEncoder encoder, NotificationRepository notificationRepository) { final User user = User.valueOf("koliving@koliving.com", encoder.encode("test1234!@"), UserRole.USER); final User user2 = User.valueOf("koliving2@koliving.com", encoder.encode("test1234!@"), UserRole.USER); diff --git a/src/main/java/com/koliving/api/my/ui/MyController.java b/src/main/java/com/koliving/api/my/ui/MyController.java index 79971807..a5bc2dbd 100644 --- a/src/main/java/com/koliving/api/my/ui/MyController.java +++ b/src/main/java/com/koliving/api/my/ui/MyController.java @@ -5,6 +5,7 @@ import com.koliving.api.room.application.RoomService; import com.koliving.api.room.application.dto.RoomResponse; import com.koliving.api.user.application.dto.NotificationResponse; +import com.koliving.api.user.domain.NotifyType; import com.koliving.api.user.domain.User; import com.koliving.api.user.application.UserService; import com.koliving.api.user.application.dto.UserResponse; @@ -20,9 +21,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -86,7 +89,8 @@ public ResponseEntity myProfile(@AuthenticationPrincipal User user responses = { @ApiResponse( responseCode = "200", - description = "좋아요 게시글 조회 성공" + description = "좋아요 게시글 조회 성공", + content = @Content(schema = @Schema(implementation = RoomResponse.class)) ), @ApiResponse( responseCode = "400", @@ -108,7 +112,8 @@ public ResponseEntity> getLikedRooms(Pageable pageable, @Auth responses = { @ApiResponse( responseCode = "200", - description = "알림 리스트 조회 성공" + description = "알림 리스트 조회 성공", + content = @Content(schema = @Schema(implementation = NotificationResponse.class)) ), @ApiResponse( responseCode = "400", @@ -117,9 +122,50 @@ public ResponseEntity> getLikedRooms(Pageable pageable, @Auth ), }) @GetMapping("/notification") - public ResponseEntity> getNotifications(@AuthenticationPrincipal User user) { - List responses = userService.getNotifications(user); + public ResponseEntity> getNotifications(@RequestParam(name = "notifyType", defaultValue = "ALL") NotifyType notifyType, @AuthenticationPrincipal User user) { + List responses = userService.getNotifications(notifyType, user); return ResponseEntity.ok() .body(responses); } + + @Operation( + summary = "알림 확인하기", + description = "알림을 확인합니다", + responses = { + @ApiResponse( + responseCode = "200", + description = "알림 확인 성공" + ), + @ApiResponse( + responseCode = "400", + description = "알림 확인 실패", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + }) + @PutMapping("/notification/{id}") + public ResponseEntity notificationConfirmed(@PathVariable Long id, @AuthenticationPrincipal User user) { + userService.notificationConfirmed(id, user); + return ResponseEntity.ok() + .build(); + } + + @Operation( + summary = "수신 새 알림 조회", + description = "수신 새 알림을 조회합니다", + responses = { + @ApiResponse( + responseCode = "200", + description = "수신 새 알림 확인 성공" + ), + @ApiResponse( + responseCode = "400", + description = "수신 새 알림 확인 실패", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + }) + @GetMapping("/notification/check") + public ResponseEntity> hasCheckedNewNotification(@AuthenticationPrincipal User user) { + List responses = userService.hasCheckedNewNotification(user); + return ResponseEntity.ok().body(responses); + } } diff --git a/src/main/java/com/koliving/api/report/application/ReportReasonRepository.java b/src/main/java/com/koliving/api/report/application/ReportReasonRepository.java new file mode 100644 index 00000000..6f36a77a --- /dev/null +++ b/src/main/java/com/koliving/api/report/application/ReportReasonRepository.java @@ -0,0 +1,11 @@ +package com.koliving.api.report.application; + +import com.koliving.api.report.domain.ReportReason; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * author : haedoang date : 2023/11/29 description : + */ +public interface ReportReasonRepository extends JpaRepository { + +} diff --git a/src/main/java/com/koliving/api/report/application/ReportService.java b/src/main/java/com/koliving/api/report/application/ReportService.java new file mode 100644 index 00000000..123ff64d --- /dev/null +++ b/src/main/java/com/koliving/api/report/application/ReportService.java @@ -0,0 +1,30 @@ +package com.koliving.api.report.application; + +import com.koliving.api.report.application.dto.ReportReasonResponse; +import com.koliving.api.report.application.dto.ReportRequest; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * author : haedoang date : 2023/11/29 description : + */ +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ReportService { + private final ReportReasonRepository reportReasonRepository; + + public List getReasons() { + return reportReasonRepository.findAll() + .stream() + .map(ReportReasonResponse::of) + .collect(Collectors.toList()); + } + + public void saveReport(ReportRequest request) { + //TODO 리포트 로직 구현 + } +} diff --git a/src/main/java/com/koliving/api/report/application/dto/ReportReasonResponse.java b/src/main/java/com/koliving/api/report/application/dto/ReportReasonResponse.java new file mode 100644 index 00000000..cd55f0a2 --- /dev/null +++ b/src/main/java/com/koliving/api/report/application/dto/ReportReasonResponse.java @@ -0,0 +1,21 @@ +package com.koliving.api.report.application.dto; + +import com.koliving.api.report.domain.ReportReason; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * author : haedoang date : 2023/11/29 description : + */ +@Schema(description = "리포트 사유 조회") +public record ReportReasonResponse( + @Schema(description = "리포트 사유 고유 ID") + Long id, + + @Schema(description = "리포트 사유 설명") + String desc +) { + + public static ReportReasonResponse of(ReportReason entity) { + return new ReportReasonResponse(entity.getId(), entity.getName()); + } +} diff --git a/src/main/java/com/koliving/api/report/application/dto/ReportRequest.java b/src/main/java/com/koliving/api/report/application/dto/ReportRequest.java new file mode 100644 index 00000000..afefbffe --- /dev/null +++ b/src/main/java/com/koliving/api/report/application/dto/ReportRequest.java @@ -0,0 +1,16 @@ +package com.koliving.api.report.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * author : haedoang date : 2023/11/29 description : + */ +@Schema(description = "리포트 사유 조회") +public record ReportRequest( + @Schema(description = "리포트 대상 룸 ID") + Long roomId, + + @Schema(description = "리포트 사유 ID") + Long reportId +) { +} diff --git a/src/main/java/com/koliving/api/report/domain/ReportReason.java b/src/main/java/com/koliving/api/report/domain/ReportReason.java new file mode 100644 index 00000000..458fcdbe --- /dev/null +++ b/src/main/java/com/koliving/api/report/domain/ReportReason.java @@ -0,0 +1,42 @@ +package com.koliving.api.report.domain; + +import static lombok.AccessLevel.PROTECTED; + +import com.koliving.api.base.domain.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +/** + * author : haedoang date : 2023/11/29 description : + */ +@Getter +@Entity(name = "TB_REPORT_REASON") +@SQLDelete(sql = "UPDATE TB_REPORT_REASON SET deleted = true WHERE id = ?") +@Where(clause = "deleted = false") +@EqualsAndHashCode(of = "id", callSuper = false) +@NoArgsConstructor(access = PROTECTED) +public class ReportReason extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; + + private ReportReason(String name) { + this.name = name; + } + + public static ReportReason of(String name) { + return new ReportReason(name); + } +} diff --git a/src/main/java/com/koliving/api/report/ui/ReportController.java b/src/main/java/com/koliving/api/report/ui/ReportController.java new file mode 100644 index 00000000..437e1d0b --- /dev/null +++ b/src/main/java/com/koliving/api/report/ui/ReportController.java @@ -0,0 +1,76 @@ +package com.koliving.api.report.ui; + + +import com.koliving.api.base.ErrorResponse; +import com.koliving.api.report.application.ReportService; +import com.koliving.api.report.application.dto.ReportReasonResponse; +import com.koliving.api.report.application.dto.ReportRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "리포트 API", description = "REPORT API") +@RequestMapping("api/v1/report") +@RestController +@RequiredArgsConstructor +public class ReportController { + private final ReportService reportService; + + @Operation( + summary = "리포트 사유 목록", + description = "리포트 사유를 요청합니다", + responses = { + @ApiResponse( + responseCode = "200", + description = "리포트 사유 요청 성공", + content = @Content(schema = @Schema(implementation = ReportReasonResponse.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "리포트 사유 요청 실패", + content = @Content(schema = @Schema(implementation = ErrorResponse.class) + ) + ) + } + ) + @GetMapping("/reasons") + public ResponseEntity> getReasons() { + final List responses = reportService.getReasons(); + return ResponseEntity.ok() + .body(responses); + } + + + @Operation( + summary = "리포트 요청", + description = "리포트를 요청합니다", + responses = { + @ApiResponse( + responseCode = "201", + description = "리포트 요청 성공" + ), + @ApiResponse( + responseCode = "400", + description = "리포트 요청 실패", + content = @Content(schema = @Schema(implementation = ErrorResponse.class) + ) + ) + } + ) + @PostMapping("/reasons") + public ResponseEntity report(@RequestBody ReportRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).build(); + } +} diff --git a/src/main/java/com/koliving/api/user/application/UserService.java b/src/main/java/com/koliving/api/user/application/UserService.java index 1e14031d..bc58a80c 100644 --- a/src/main/java/com/koliving/api/user/application/UserService.java +++ b/src/main/java/com/koliving/api/user/application/UserService.java @@ -1,5 +1,7 @@ package com.koliving.api.user.application; +import static com.koliving.api.user.domain.NotifyType.RECEIVE; + import com.koliving.api.base.ServiceError; import com.koliving.api.base.exception.KolivingServiceException; import com.koliving.api.file.domain.ImageFile; @@ -12,9 +14,9 @@ import com.koliving.api.user.domain.User; import com.koliving.api.user.infra.NotificationRepository; import com.koliving.api.user.infra.UserRepository; +import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; -import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -86,10 +88,42 @@ public UserResponse findById(Long id) { return UserResponse.valueOf(user); } - public List getNotifications(User user) { - final List receives = notificationRepository.findAllByReceiverId(user.getId()); - final List sent = notificationRepository.findAllBySenderId(user.getId()); + public List getNotifications(NotifyType notifyType, User user) { + if (notifyType.isSend()) { + return notificationRepository.findBySenderId(user.getId(), getOneMonthBefore()) + .stream() + .map(notification -> NotificationResponse.of(notifyType, notification)) + .collect(Collectors.toList()); + } + + if (notifyType.isReceive()) { + return notificationRepository.findByReceiverId(user.getId(), getOneMonthBefore()) + .stream() + .map(notification -> NotificationResponse.of(notifyType, notification)) + .collect(Collectors.toList()); + } + + final List receives = notificationRepository.findByReceiverId(user.getId(), getOneMonthBefore()); + final List sent = notificationRepository.findBySenderId(user.getId(), getOneMonthBefore()); return NotificationResponse.ofList(receives, sent); } + + @Transactional + public void notificationConfirmed(Long id, User user) { + final Notification notification = notificationRepository.findById(id) + .orElseThrow(() -> new KolivingServiceException(ServiceError.RECORD_NOT_EXIST)); + notification.confirm(user); + } + + public List hasCheckedNewNotification(User user) { + return notificationRepository.findNotConfirmedByReceiverId(user.getId(), getOneMonthBefore()) + .stream() + .map(notification -> NotificationResponse.of(RECEIVE, notification)) + .collect(Collectors.toList()); + } + + public LocalDateTime getOneMonthBefore() { + return LocalDateTime.now().minusMonths(1); + } } diff --git a/src/main/java/com/koliving/api/user/application/dto/NotificationResponse.java b/src/main/java/com/koliving/api/user/application/dto/NotificationResponse.java index aa28491e..38088481 100644 --- a/src/main/java/com/koliving/api/user/application/dto/NotificationResponse.java +++ b/src/main/java/com/koliving/api/user/application/dto/NotificationResponse.java @@ -16,6 +16,8 @@ * author : haedoang date : 2023/11/05 description : */ public record NotificationResponse( + @Schema(description = "알림 고유 ID") + Long id, @Schema(description = "알림 타입[SEND, RECEIVE]") NotifyType type, @@ -23,25 +25,34 @@ public record NotificationResponse( @Schema(description = "알림 발/수신자") String userName, + @Schema(description = "알림 발/수신자 프로필") + String imageProfile, + @Schema(description = "알림 생성일자") - LocalDateTime createdAt + LocalDateTime createdAt, + + @Schema(description = "확인 여부") + Boolean confirm ) { public static NotificationResponse of(NotifyType type, Notification entity) { return new NotificationResponse( + entity.getId(), type, - type.isSend() ? entity.getSender().getFullName() : entity.getReceiver().getFullName(), - entity.getCreatedAt() + type.isReceive() ? entity.getSender().getFullName() : entity.getReceiver().getFullName(), + type.isReceive() ? entity.getSender().getImageProfile() : entity.getReceiver().getImageProfile(), + entity.getCreatedAt(), + entity.getConfirm() ); } public static List ofList(List receives, List sent) { return Stream.concat( - receives.stream() - .map(notification -> of(RECEIVE, notification) - ), - sent.stream() - .map(notification -> of(SEND, notification)) + receives.stream() + .map(notification -> of(RECEIVE, notification) + ), + sent.stream() + .map(notification -> of(SEND, notification)) ).sorted(Comparator.comparing(NotificationResponse::createdAt).reversed()) .collect(Collectors.toList()); } diff --git a/src/main/java/com/koliving/api/user/domain/Notification.java b/src/main/java/com/koliving/api/user/domain/Notification.java index fb71bcae..3feaed00 100644 --- a/src/main/java/com/koliving/api/user/domain/Notification.java +++ b/src/main/java/com/koliving/api/user/domain/Notification.java @@ -1,6 +1,10 @@ package com.koliving.api.user.domain; +import static com.koliving.api.base.ServiceError.FORBIDDEN; + import com.koliving.api.base.domain.BaseEntity; +import com.koliving.api.base.exception.KolivingServiceException; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -25,7 +29,7 @@ @ToString @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Notification extends BaseEntity { +public class Notification extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -39,11 +43,21 @@ public class Notification extends BaseEntity { @JoinColumn(name = "RECEIVER_ID") private User receiver; + @Column + private Boolean confirm = Boolean.FALSE; + private Notification(User sender, User receiver) { this.sender = sender; this.receiver = receiver; } + public void confirm(User user) { + if (!receiver.equals(user)) { + throw new KolivingServiceException(FORBIDDEN); + } + this.confirm = Boolean.TRUE; + } + public static Notification of(User sender, User receiver) { return new Notification(sender, receiver); } diff --git a/src/main/java/com/koliving/api/user/domain/NotifyType.java b/src/main/java/com/koliving/api/user/domain/NotifyType.java index 508c59b2..aeeb3c04 100644 --- a/src/main/java/com/koliving/api/user/domain/NotifyType.java +++ b/src/main/java/com/koliving/api/user/domain/NotifyType.java @@ -4,13 +4,14 @@ * author : haedoang date : 2023/11/05 description : */ public enum NotifyType { + ALL, SEND, RECEIVE; + public boolean isAll() { return this == ALL; } public boolean isSend() { return this == SEND; } - public boolean isReceive() { return this == RECEIVE; } diff --git a/src/main/java/com/koliving/api/user/domain/User.java b/src/main/java/com/koliving/api/user/domain/User.java index 7dc11c53..4422a41a 100644 --- a/src/main/java/com/koliving/api/user/domain/User.java +++ b/src/main/java/com/koliving/api/user/domain/User.java @@ -47,7 +47,7 @@ @DynamicInsert @DynamicUpdate @Getter -@ToString +@ToString(of = "id") @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class User extends BaseEntity implements UserDetails { diff --git a/src/main/java/com/koliving/api/user/infra/NotificationRepository.java b/src/main/java/com/koliving/api/user/infra/NotificationRepository.java index f7d056a5..2d8fb62d 100644 --- a/src/main/java/com/koliving/api/user/infra/NotificationRepository.java +++ b/src/main/java/com/koliving/api/user/infra/NotificationRepository.java @@ -1,16 +1,24 @@ package com.koliving.api.user.infra; import com.koliving.api.user.domain.Notification; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; /** * author : haedoang date : 2023/11/05 description : */ public interface NotificationRepository extends JpaRepository { - List findAllBySenderId(Long senderId); + @Query("select n from TB_NOTIFICATION n where n.sender.id = :senderId and n.createdAt >= :datetime") + List findBySenderId(@Param("senderId") Long senderId, @Param("datetime") LocalDateTime datetime); - List findAllByReceiverId(Long senderId); + @Query("select n from TB_NOTIFICATION n where n.receiver.id = :receiverId and n.createdAt >= :datetime") + List findByReceiverId(@Param("receiverId") Long receiverId, @Param("datetime") LocalDateTime datetime); + + @Query("select n from TB_NOTIFICATION n where n.receiver.id = :receiverId and n.createdAt >= :datetime and n.confirm=false") + List findNotConfirmedByReceiverId(@Param("receiverId") Long receiverId, @Param("datetime") LocalDateTime datetime); } diff --git a/src/test/java/com/koliving/api/report/application/ReportReasonRepositoryTest.java b/src/test/java/com/koliving/api/report/application/ReportReasonRepositoryTest.java new file mode 100644 index 00000000..0c1e656f --- /dev/null +++ b/src/test/java/com/koliving/api/report/application/ReportReasonRepositoryTest.java @@ -0,0 +1,32 @@ +package com.koliving.api.report.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import com.koliving.api.report.domain.ReportReason; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +/** + * author : haedoang date : 2023/11/29 description : + */ +@DataJpaTest +class ReportReasonRepositoryTest { + @Autowired + private ReportReasonRepository reportReasonRepository; + + @Test + @DisplayName("리포트 사유 생성하기") + void create() { + // given + final ReportReason given = ReportReason.of("Not a Real Place"); + + // when + final ReportReason actual = reportReasonRepository.save(given); + + // then + assertThat(actual.getId()).isNotNull(); + } +} \ No newline at end of file