From e7b79eb5290ede01c0fe3b6edb947cc2f27361fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 8 Aug 2026 00:10:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=EB=B0=A9/=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D/=ED=88=AC=ED=91=9C/=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=EC=8B=A0=EA=B3=A0=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(=EC=8B=A0=EA=B3=A0=20=ED=9A=9F=EC=88=98?= =?UTF-8?q?=20=EC=A6=9D=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comment, Feed에 이어 Room, Record, Vote, AttendanceCheck 도메인에도 신고 API를 추가하고 신고 횟수만 증가시키도록 구현. - Room, AttendanceCheck: report_count 컬럼이 없어 마이그레이션 추가 - Record, Vote: posts 테이블(단일 테이블 상속)에 Feed가 쓰던 report_count 컬럼이 이미 있어 엔티티 매핑만 추가 - posts.report_count의 기존 NULL 값(Record/Vote)을 0으로 백필하고 rooms/attendance_checks와 함께 NOT NULL 제약 추가 Co-Authored-By: Claude Sonnet 5 --- .../swagger/SwaggerResponseDescription.java | 13 ++ .../feed/adapter/out/jpa/FeedJpaEntity.java | 2 +- .../adapter/in/web/RoomCommandController.java | 14 ++ .../in/web/response/RoomReportResponse.java | 12 ++ .../room/adapter/out/jpa/RoomJpaEntity.java | 5 + .../room/adapter/out/mapper/RoomMapper.java | 1 + .../port/in/RoomReportUseCase.java | 7 + .../port/in/dto/RoomReportResult.java | 10 ++ .../service/RoomReportService.java | 26 ++++ .../java/konkuk/thip/room/domain/Room.java | 8 ++ .../in/web/RoomPostCommandController.java | 42 ++++++ .../AttendanceCheckReportResponse.java | 12 ++ .../in/web/response/RecordReportResponse.java | 12 ++ .../in/web/response/VoteReportResponse.java | 12 ++ .../out/jpa/AttendanceCheckJpaEntity.java | 10 ++ .../adapter/out/jpa/RecordJpaEntity.java | 7 +- .../adapter/out/jpa/VoteJpaEntity.java | 7 +- .../out/mapper/AttendanceCheckMapper.java | 1 + .../adapter/out/mapper/RecordMapper.java | 2 + .../adapter/out/mapper/VoteMapper.java | 2 + ...endanceCheckCommandPersistenceAdapter.java | 9 ++ .../port/in/AttendanceCheckReportUseCase.java | 7 + .../port/in/RecordReportUseCase.java | 7 + .../port/in/VoteReportUseCase.java | 7 + .../AttendanceCheckReportResult.java | 10 ++ .../in/dto/record/RecordReportResult.java | 10 ++ .../port/in/dto/vote/VoteReportResult.java | 10 ++ .../port/out/AttendanceCheckCommandPort.java | 2 + .../service/AttendanceCheckReportService.java | 26 ++++ .../service/RecordReportService.java | 26 ++++ .../service/VoteReportService.java | 26 ++++ .../thip/roompost/domain/AttendanceCheck.java | 8 ++ .../konkuk/thip/roompost/domain/Record.java | 7 + .../konkuk/thip/roompost/domain/Vote.java | 7 + ...t_count_to_rooms_and_attendance_checks.sql | 12 ++ .../adapter/in/web/RoomReportApiTest.java | 113 ++++++++++++++++ .../in/web/AttendanceCheckReportApiTest.java | 125 ++++++++++++++++++ .../adapter/in/web/RecordReportApiTest.java | 119 +++++++++++++++++ .../adapter/in/web/VoteReportApiTest.java | 119 +++++++++++++++++ 39 files changed, 852 insertions(+), 3 deletions(-) create mode 100644 src/main/java/konkuk/thip/room/adapter/in/web/response/RoomReportResponse.java create mode 100644 src/main/java/konkuk/thip/room/application/port/in/RoomReportUseCase.java create mode 100644 src/main/java/konkuk/thip/room/application/port/in/dto/RoomReportResult.java create mode 100644 src/main/java/konkuk/thip/room/application/service/RoomReportService.java create mode 100644 src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckReportResponse.java create mode 100644 src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordReportResponse.java create mode 100644 src/main/java/konkuk/thip/roompost/adapter/in/web/response/VoteReportResponse.java create mode 100644 src/main/java/konkuk/thip/roompost/application/port/in/AttendanceCheckReportUseCase.java create mode 100644 src/main/java/konkuk/thip/roompost/application/port/in/RecordReportUseCase.java create mode 100644 src/main/java/konkuk/thip/roompost/application/port/in/VoteReportUseCase.java create mode 100644 src/main/java/konkuk/thip/roompost/application/port/in/dto/attendancecheck/AttendanceCheckReportResult.java create mode 100644 src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordReportResult.java create mode 100644 src/main/java/konkuk/thip/roompost/application/port/in/dto/vote/VoteReportResult.java create mode 100644 src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckReportService.java create mode 100644 src/main/java/konkuk/thip/roompost/application/service/RecordReportService.java create mode 100644 src/main/java/konkuk/thip/roompost/application/service/VoteReportService.java create mode 100644 src/main/resources/db/migration/V260807__Add_report_count_to_rooms_and_attendance_checks.sql create mode 100644 src/test/java/konkuk/thip/room/adapter/in/web/RoomReportApiTest.java create mode 100644 src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckReportApiTest.java create mode 100644 src/test/java/konkuk/thip/roompost/adapter/in/web/RecordReportApiTest.java create mode 100644 src/test/java/konkuk/thip/roompost/adapter/in/web/VoteReportApiTest.java diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 270fbb886..e976f2a72 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -127,6 +127,9 @@ public enum SwaggerResponseDescription { ROOM_PARTICIPANT_NOT_FOUND, ROOM_HOST_CANNOT_LEAVE ))), + ROOM_REPORT(new LinkedHashSet<>(Set.of( + ROOM_NOT_FOUND + ))), // Record @@ -179,6 +182,9 @@ public enum SwaggerResponseDescription { USER_NOT_FOUND, ROOM_ACCESS_FORBIDDEN ))), + RECORD_REPORT(new LinkedHashSet<>(Set.of( + RECORD_NOT_FOUND + ))), // Vote VOTE_CREATE(new LinkedHashSet<>(Set.of( @@ -213,6 +219,9 @@ public enum SwaggerResponseDescription { ROOM_IS_EXPIRED, ROOM_NOT_IN_PROGRESS ))), + VOTE_REPORT(new LinkedHashSet<>(Set.of( + VOTE_NOT_FOUND + ))), // FEED @@ -373,6 +382,10 @@ public enum SwaggerResponseDescription { ROOM_NOT_IN_PROGRESS ))), + ATTENDANCE_CHECK_REPORT(new LinkedHashSet<>(Set.of( + ATTENDANCE_CHECK_NOT_FOUND + ))), + // Notiification FCM_TOKEN_REGISTER(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, diff --git a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java index f9427b0a8..7c3fb2f29 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java @@ -28,7 +28,7 @@ public class FeedJpaEntity extends PostJpaEntity { @Column(name = "is_public") private Boolean isPublic; - @Column(name = "report_count") + @Column(name = "report_count", nullable = false) private int reportCount = 0; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java index d9a72c804..b5b507c57 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java @@ -15,10 +15,12 @@ import konkuk.thip.room.adapter.in.web.response.RoomJoinResponse; import konkuk.thip.room.adapter.in.web.response.RoomPostIsLikeResponse; import konkuk.thip.room.adapter.in.web.response.RoomRecruitCloseResponse; +import konkuk.thip.room.adapter.in.web.response.RoomReportResponse; import konkuk.thip.room.application.port.in.RoomCreateUseCase; import konkuk.thip.room.application.port.in.RoomJoinUseCase; import konkuk.thip.room.application.port.in.RoomParticipantDeleteUseCase; import konkuk.thip.room.application.port.in.RoomRecruitCloseUseCase; +import konkuk.thip.room.application.port.in.RoomReportUseCase; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -34,6 +36,7 @@ public class RoomCommandController { private final RoomRecruitCloseUseCase roomRecruitCloseUsecase; private final RoomParticipantDeleteUseCase roomParticipantDeleteUseCase; private final PostLikeUseCase postLikeUseCase; + private final RoomReportUseCase roomReportUseCase; /** * 방 생성 요청 @@ -113,4 +116,15 @@ public BaseResponse deleteRoomParticipant( @Parameter(description = "나갈 방의 ID", example = "1") @PathVariable final Long roomId) { return BaseResponse.ok(roomParticipantDeleteUseCase.leaveRoom(userId, roomId)); } + + @Operation( + summary = "모임방 신고", + description = "사용자가 모임방을 신고합니다. 신고 횟수만 증가하며 별도의 처리는 이루어지지 않습니다." + ) + @ExceptionDescription(ROOM_REPORT) + @PostMapping("/rooms/{roomId}/report") + public BaseResponse reportRoom( + @Parameter(description = "신고하려는 방 ID", example = "1") @PathVariable("roomId") final Long roomId) { + return BaseResponse.ok(RoomReportResponse.of(roomReportUseCase.reportRoom(roomId))); + } } diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomReportResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomReportResponse.java new file mode 100644 index 000000000..661f5d392 --- /dev/null +++ b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomReportResponse.java @@ -0,0 +1,12 @@ +package konkuk.thip.room.adapter.in.web.response; + +import konkuk.thip.room.application.port.in.dto.RoomReportResult; + +public record RoomReportResponse( + Long roomId, + int reportCount +) { + public static RoomReportResponse of(RoomReportResult roomReportResult) { + return new RoomReportResponse(roomReportResult.roomId(), roomReportResult.reportCount()); + } +} diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java index c5822ba89..686e802e6 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java +++ b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java @@ -69,6 +69,10 @@ public class RoomJpaEntity extends BaseJpaEntity { @Column(nullable = false) private Category category; + @Builder.Default + @Column(name = "report_count", nullable = false) + private int reportCount = 0; + public RoomJpaEntity updateFrom(Room room) { this.title = room.getTitle(); this.description = room.getDescription(); @@ -80,6 +84,7 @@ public RoomJpaEntity updateFrom(Room room) { this.recruitCount = room.getRecruitCount(); this.memberCount = room.getMemberCount(); this.roomStatus = room.getRoomStatus(); + this.reportCount = room.getReportCount(); return this; } diff --git a/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java b/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java index 5dd3ebe81..51d20a3f0 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java +++ b/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java @@ -40,6 +40,7 @@ public Room toDomainEntity(RoomJpaEntity roomJpaEntity) { .roomStatus(roomJpaEntity.getRoomStatus()) .bookId(roomJpaEntity.getBookJpaEntity().getBookId()) .category(roomJpaEntity.getCategory()) + .reportCount(roomJpaEntity.getReportCount()) .createdAt(roomJpaEntity.getCreatedAt()) .modifiedAt(roomJpaEntity.getModifiedAt()) .status(roomJpaEntity.getStatus()) diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomReportUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomReportUseCase.java new file mode 100644 index 000000000..12e8fb0a1 --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomReportUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.room.application.port.in; + +import konkuk.thip.room.application.port.in.dto.RoomReportResult; + +public interface RoomReportUseCase { + RoomReportResult reportRoom(Long roomId); +} diff --git a/src/main/java/konkuk/thip/room/application/port/in/dto/RoomReportResult.java b/src/main/java/konkuk/thip/room/application/port/in/dto/RoomReportResult.java new file mode 100644 index 000000000..460cc8eb4 --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/dto/RoomReportResult.java @@ -0,0 +1,10 @@ +package konkuk.thip.room.application.port.in.dto; + +public record RoomReportResult( + Long roomId, + int reportCount +) { + public static RoomReportResult of(Long roomId, int reportCount) { + return new RoomReportResult(roomId, reportCount); + } +} diff --git a/src/main/java/konkuk/thip/room/application/service/RoomReportService.java b/src/main/java/konkuk/thip/room/application/service/RoomReportService.java new file mode 100644 index 000000000..d598ef516 --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/service/RoomReportService.java @@ -0,0 +1,26 @@ +package konkuk.thip.room.application.service; + +import konkuk.thip.room.application.port.in.RoomReportUseCase; +import konkuk.thip.room.application.port.in.dto.RoomReportResult; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.domain.Room; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class RoomReportService implements RoomReportUseCase { + + private final RoomCommandPort roomCommandPort; + + @Override + @Transactional + public RoomReportResult reportRoom(Long roomId) { + Room room = roomCommandPort.getByIdOrThrow(roomId); + room.increaseReportCount(); + roomCommandPort.update(room); + + return RoomReportResult.of(room.getId(), room.getReportCount()); + } +} diff --git a/src/main/java/konkuk/thip/room/domain/Room.java b/src/main/java/konkuk/thip/room/domain/Room.java index 490856180..d79e57a5d 100644 --- a/src/main/java/konkuk/thip/room/domain/Room.java +++ b/src/main/java/konkuk/thip/room/domain/Room.java @@ -5,6 +5,7 @@ import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.room.domain.value.Category; import konkuk.thip.room.domain.value.RoomStatus; +import lombok.Builder; import lombok.Getter; import lombok.experimental.SuperBuilder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -46,6 +47,9 @@ public class Room extends BaseDomainEntity { private Category category; + @Builder.Default + private int reportCount = 0; + public static Room withoutId(String title, String description, boolean isPublic, String password, LocalDate startDate, LocalDate endDate, int recruitCount, Long bookId, Category category) { validateVisibilityPasswordRule(isPublic, password); validateDates(startDate, endDate); @@ -182,4 +186,8 @@ public void validateRoomInProgress() { } } + public void increaseReportCount() { + reportCount++; + } + } diff --git a/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java b/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java index cfc15e1c7..e4b307259 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java +++ b/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java @@ -32,9 +32,13 @@ public class RoomPostCommandController { private final AttendanceCheckCreateUseCase attendanceCheckCreateUseCase; private final AttendanceCheckDeleteUseCase attendanceCheckDeleteUseCase; + private final AttendanceCheckReportUseCase attendanceCheckReportUseCase; private final RecordReviewCreateUseCase recordReviewCreateUseCase; + private final RecordReportUseCase recordReportUseCase; + private final VoteReportUseCase voteReportUseCase; + /** * 기록 관련 */ @@ -85,6 +89,18 @@ public BaseResponse updateRecord( )); } + @Operation( + summary = "기록 신고", + description = "사용자가 기록을 신고합니다. 신고 횟수만 증가하며 별도의 처리는 이루어지지 않습니다." + ) + @ExceptionDescription(RECORD_REPORT) + @PostMapping("/rooms/{roomId}/record/{recordId}/report") + public BaseResponse reportRecord( + @Parameter(description = "신고할 방 ID", example = "1") @PathVariable("roomId") final Long roomId, + @Parameter(description = "신고하려는 기록 ID", example = "1") @PathVariable("recordId") final Long recordId) { + return BaseResponse.ok(RecordReportResponse.of(recordReportUseCase.reportRecord(recordId))); + } + /** * 투표 관련 */ @@ -149,6 +165,18 @@ public BaseResponse updateVote( )); } + @Operation( + summary = "투표 신고", + description = "사용자가 투표를 신고합니다. 신고 횟수만 증가하며 별도의 처리는 이루어지지 않습니다." + ) + @ExceptionDescription(VOTE_REPORT) + @PostMapping("/rooms/{roomId}/vote/{voteId}/report") + public BaseResponse reportVote( + @Parameter(description = "신고할 방 ID", example = "1") @PathVariable("roomId") final Long roomId, + @Parameter(description = "신고하려는 투표 ID", example = "1") @PathVariable("voteId") final Long voteId) { + return BaseResponse.ok(VoteReportResponse.of(voteReportUseCase.reportVote(voteId))); + } + /** * 오늘의 한마디 관련 */ @@ -182,6 +210,20 @@ public BaseResponse deleteAttendanceCheck( )); } + @Operation( + summary = "오늘의 한마디 신고", + description = "사용자가 오늘의 한마디를 신고합니다. 신고 횟수만 증가하며 별도의 처리는 이루어지지 않습니다." + ) + @ExceptionDescription(ATTENDANCE_CHECK_REPORT) + @PostMapping("/rooms/{roomId}/daily-greeting/{attendanceCheckId}/report") + public BaseResponse reportAttendanceCheck( + @Parameter(description = "신고할 방 ID", example = "1") @PathVariable("roomId") final Long roomId, + @Parameter(description = "신고하려는 오늘의 한마디 ID", example = "1") @PathVariable("attendanceCheckId") final Long attendanceCheckId) { + return BaseResponse.ok(AttendanceCheckReportResponse.of( + attendanceCheckReportUseCase.reportAttendanceCheck(attendanceCheckId) + )); + } + @Operation( summary = "AI 기반 기록 독후감 생성", description = "AI를 활용하여 사용자가 작성한 기록을 바탕으로 독후감을 생성합니다." diff --git a/src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckReportResponse.java b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckReportResponse.java new file mode 100644 index 000000000..b76e97602 --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckReportResponse.java @@ -0,0 +1,12 @@ +package konkuk.thip.roompost.adapter.in.web.response; + +import konkuk.thip.roompost.application.port.in.dto.attendancecheck.AttendanceCheckReportResult; + +public record AttendanceCheckReportResponse( + Long attendanceCheckId, + int reportCount +) { + public static AttendanceCheckReportResponse of(AttendanceCheckReportResult attendanceCheckReportResult) { + return new AttendanceCheckReportResponse(attendanceCheckReportResult.attendanceCheckId(), attendanceCheckReportResult.reportCount()); + } +} diff --git a/src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordReportResponse.java b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordReportResponse.java new file mode 100644 index 000000000..0371f8e81 --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordReportResponse.java @@ -0,0 +1,12 @@ +package konkuk.thip.roompost.adapter.in.web.response; + +import konkuk.thip.roompost.application.port.in.dto.record.RecordReportResult; + +public record RecordReportResponse( + Long recordId, + int reportCount +) { + public static RecordReportResponse of(RecordReportResult recordReportResult) { + return new RecordReportResponse(recordReportResult.recordId(), recordReportResult.reportCount()); + } +} diff --git a/src/main/java/konkuk/thip/roompost/adapter/in/web/response/VoteReportResponse.java b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/VoteReportResponse.java new file mode 100644 index 000000000..db94a27ad --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/VoteReportResponse.java @@ -0,0 +1,12 @@ +package konkuk.thip.roompost.adapter.in.web.response; + +import konkuk.thip.roompost.application.port.in.dto.vote.VoteReportResult; + +public record VoteReportResponse( + Long voteId, + int reportCount +) { + public static VoteReportResponse of(VoteReportResult voteReportResult) { + return new VoteReportResponse(voteReportResult.voteId(), voteReportResult.reportCount()); + } +} diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java index 0421920a3..da8672132 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.roompost.domain.AttendanceCheck; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.*; import org.hibernate.annotations.SQLDelete; @@ -32,4 +33,13 @@ public class AttendanceCheckJpaEntity extends BaseJpaEntity { @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; + @Builder.Default + @Column(name = "report_count", nullable = false) + private int reportCount = 0; + + public AttendanceCheckJpaEntity updateFrom(AttendanceCheck attendanceCheck) { + this.reportCount = attendanceCheck.getReportCount(); + return this; + } + } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java index 319acf7d6..7e793b3e3 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java @@ -27,12 +27,16 @@ public class RecordJpaEntity extends PostJpaEntity { @JoinColumn(name = "room_id") // FEED 로 인해 nullable = true로 설정 private RoomJpaEntity roomJpaEntity; + @Column(name = "report_count", nullable = false) + private int reportCount; + @Builder - public RecordJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Integer page, boolean isOverview, RoomJpaEntity roomJpaEntity) { + public RecordJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Integer page, boolean isOverview, RoomJpaEntity roomJpaEntity, int reportCount) { super(content, likeCount, commentCount, userJpaEntity); this.page = page; this.isOverview = isOverview; this.roomJpaEntity = roomJpaEntity; + this.reportCount = reportCount; } public RecordJpaEntity updateFrom(Record record) { @@ -41,6 +45,7 @@ public RecordJpaEntity updateFrom(Record record) { this.commentCount = record.getCommentCount(); this.page = record.getPage(); this.isOverview = record.isOverview(); + this.reportCount = record.getReportCount(); return this; } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java index 03a6b77ba..b341fcb20 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java @@ -27,12 +27,16 @@ public class VoteJpaEntity extends PostJpaEntity { @JoinColumn(name = "room_id") // FEED 로 인해 nullable = true로 설정 private RoomJpaEntity roomJpaEntity; + @Column(name = "report_count", nullable = false) + private int reportCount; + @Builder - public VoteJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Integer page, boolean isOverview, RoomJpaEntity roomJpaEntity) { + public VoteJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Integer page, boolean isOverview, RoomJpaEntity roomJpaEntity, int reportCount) { super(content, likeCount, commentCount, userJpaEntity); this.page = page; this.isOverview = isOverview; this.roomJpaEntity = roomJpaEntity; + this.reportCount = reportCount; } public VoteJpaEntity updateFrom(Vote vote) { @@ -41,6 +45,7 @@ public VoteJpaEntity updateFrom(Vote vote) { this.commentCount = vote.getCommentCount(); this.page = vote.getPage(); this.isOverview = vote.isOverview(); + this.reportCount = vote.getReportCount(); return this; } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/mapper/AttendanceCheckMapper.java b/src/main/java/konkuk/thip/roompost/adapter/out/mapper/AttendanceCheckMapper.java index cbf34b629..b7594daa9 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/mapper/AttendanceCheckMapper.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/mapper/AttendanceCheckMapper.java @@ -23,6 +23,7 @@ public AttendanceCheck toDomainEntity(AttendanceCheckJpaEntity attendanceCheckJp .todayComment(attendanceCheckJpaEntity.getTodayComment()) .roomId(attendanceCheckJpaEntity.getRoomJpaEntity().getRoomId()) .creatorId(attendanceCheckJpaEntity.getUserJpaEntity().getUserId()) + .reportCount(attendanceCheckJpaEntity.getReportCount()) .createdAt(attendanceCheckJpaEntity.getCreatedAt()) .modifiedAt(attendanceCheckJpaEntity.getModifiedAt()) .status(attendanceCheckJpaEntity.getStatus()) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/mapper/RecordMapper.java b/src/main/java/konkuk/thip/roompost/adapter/out/mapper/RecordMapper.java index 5e0e9911a..ee62a0e97 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/mapper/RecordMapper.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/mapper/RecordMapper.java @@ -18,6 +18,7 @@ public RecordJpaEntity toJpaEntity(Record record, UserJpaEntity userJpaEntity, R .page(record.getPage()) .isOverview(record.isOverview()) .roomJpaEntity(roomJpaEntity) + .reportCount(record.getReportCount()) .build(); } @@ -31,6 +32,7 @@ public Record toDomainEntity(RecordJpaEntity recordJpaEntity) { .roomId(recordJpaEntity.getRoomJpaEntity().getRoomId()) .likeCount(recordJpaEntity.getLikeCount()) .commentCount(recordJpaEntity.getCommentCount()) + .reportCount(recordJpaEntity.getReportCount()) .createdAt(recordJpaEntity.getCreatedAt()) .modifiedAt(recordJpaEntity.getModifiedAt()) .status(recordJpaEntity.getStatus()) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/mapper/VoteMapper.java b/src/main/java/konkuk/thip/roompost/adapter/out/mapper/VoteMapper.java index 0d44ee3b5..e22ef6c9f 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/mapper/VoteMapper.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/mapper/VoteMapper.java @@ -18,6 +18,7 @@ public VoteJpaEntity toJpaEntity(Vote vote, UserJpaEntity userJpaEntity, RoomJpa .likeCount(vote.getLikeCount()) .commentCount(vote.getCommentCount()) .roomJpaEntity(roomJpaEntity) + .reportCount(vote.getReportCount()) .build(); } @@ -31,6 +32,7 @@ public Vote toDomainEntity(VoteJpaEntity voteJpaEntity) { .likeCount(voteJpaEntity.getLikeCount()) .commentCount(voteJpaEntity.getCommentCount()) .roomId(voteJpaEntity.getRoomJpaEntity().getRoomId()) + .reportCount(voteJpaEntity.getReportCount()) .createdAt(voteJpaEntity.getCreatedAt()) .modifiedAt(voteJpaEntity.getModifiedAt()) .status(voteJpaEntity.getStatus()) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java index e48ff3e9c..c0f863010 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java @@ -47,6 +47,15 @@ public Optional findById(Long id) { .map(attendanceCheckMapper::toDomainEntity); } + @Override + public void update(AttendanceCheck attendanceCheck) { + AttendanceCheckJpaEntity attendanceCheckJpaEntity = attendanceCheckJpaRepository.findByAttendanceCheckId(attendanceCheck.getId()).orElseThrow( + () -> new EntityNotFoundException(ATTENDANCE_CHECK_NOT_FOUND) + ); + + attendanceCheckJpaRepository.save(attendanceCheckJpaEntity.updateFrom(attendanceCheck)); + } + @Override public void delete(AttendanceCheck attendanceCheck) { AttendanceCheckJpaEntity attendanceCheckJpaEntity = attendanceCheckJpaRepository.findByAttendanceCheckId(attendanceCheck.getId()).orElseThrow( diff --git a/src/main/java/konkuk/thip/roompost/application/port/in/AttendanceCheckReportUseCase.java b/src/main/java/konkuk/thip/roompost/application/port/in/AttendanceCheckReportUseCase.java new file mode 100644 index 000000000..207116f2f --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/port/in/AttendanceCheckReportUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.roompost.application.port.in; + +import konkuk.thip.roompost.application.port.in.dto.attendancecheck.AttendanceCheckReportResult; + +public interface AttendanceCheckReportUseCase { + AttendanceCheckReportResult reportAttendanceCheck(Long attendanceCheckId); +} diff --git a/src/main/java/konkuk/thip/roompost/application/port/in/RecordReportUseCase.java b/src/main/java/konkuk/thip/roompost/application/port/in/RecordReportUseCase.java new file mode 100644 index 000000000..471c5a6cf --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/port/in/RecordReportUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.roompost.application.port.in; + +import konkuk.thip.roompost.application.port.in.dto.record.RecordReportResult; + +public interface RecordReportUseCase { + RecordReportResult reportRecord(Long recordId); +} diff --git a/src/main/java/konkuk/thip/roompost/application/port/in/VoteReportUseCase.java b/src/main/java/konkuk/thip/roompost/application/port/in/VoteReportUseCase.java new file mode 100644 index 000000000..1e34e1825 --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/port/in/VoteReportUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.roompost.application.port.in; + +import konkuk.thip.roompost.application.port.in.dto.vote.VoteReportResult; + +public interface VoteReportUseCase { + VoteReportResult reportVote(Long voteId); +} diff --git a/src/main/java/konkuk/thip/roompost/application/port/in/dto/attendancecheck/AttendanceCheckReportResult.java b/src/main/java/konkuk/thip/roompost/application/port/in/dto/attendancecheck/AttendanceCheckReportResult.java new file mode 100644 index 000000000..794e460cd --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/port/in/dto/attendancecheck/AttendanceCheckReportResult.java @@ -0,0 +1,10 @@ +package konkuk.thip.roompost.application.port.in.dto.attendancecheck; + +public record AttendanceCheckReportResult( + Long attendanceCheckId, + int reportCount +) { + public static AttendanceCheckReportResult of(Long attendanceCheckId, int reportCount) { + return new AttendanceCheckReportResult(attendanceCheckId, reportCount); + } +} diff --git a/src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordReportResult.java b/src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordReportResult.java new file mode 100644 index 000000000..82d733d99 --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordReportResult.java @@ -0,0 +1,10 @@ +package konkuk.thip.roompost.application.port.in.dto.record; + +public record RecordReportResult( + Long recordId, + int reportCount +) { + public static RecordReportResult of(Long recordId, int reportCount) { + return new RecordReportResult(recordId, reportCount); + } +} diff --git a/src/main/java/konkuk/thip/roompost/application/port/in/dto/vote/VoteReportResult.java b/src/main/java/konkuk/thip/roompost/application/port/in/dto/vote/VoteReportResult.java new file mode 100644 index 000000000..75a07798b --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/port/in/dto/vote/VoteReportResult.java @@ -0,0 +1,10 @@ +package konkuk.thip.roompost.application.port.in.dto.vote; + +public record VoteReportResult( + Long voteId, + int reportCount +) { + public static VoteReportResult of(Long voteId, int reportCount) { + return new VoteReportResult(voteId, reportCount); + } +} diff --git a/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckCommandPort.java b/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckCommandPort.java index 2df49ed18..a3772ffea 100644 --- a/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckCommandPort.java +++ b/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckCommandPort.java @@ -18,6 +18,8 @@ default AttendanceCheck getByIdOrThrow(Long id) { .orElseThrow(() -> new EntityNotFoundException(ATTENDANCE_CHECK_NOT_FOUND)); } + void update(AttendanceCheck attendanceCheck); + void delete(AttendanceCheck attendanceCheck); void deleteAllByUserId(Long userId); diff --git a/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckReportService.java b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckReportService.java new file mode 100644 index 000000000..3985c4e29 --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckReportService.java @@ -0,0 +1,26 @@ +package konkuk.thip.roompost.application.service; + +import konkuk.thip.roompost.application.port.in.AttendanceCheckReportUseCase; +import konkuk.thip.roompost.application.port.in.dto.attendancecheck.AttendanceCheckReportResult; +import konkuk.thip.roompost.application.port.out.AttendanceCheckCommandPort; +import konkuk.thip.roompost.domain.AttendanceCheck; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AttendanceCheckReportService implements AttendanceCheckReportUseCase { + + private final AttendanceCheckCommandPort attendanceCheckCommandPort; + + @Override + @Transactional + public AttendanceCheckReportResult reportAttendanceCheck(Long attendanceCheckId) { + AttendanceCheck attendanceCheck = attendanceCheckCommandPort.getByIdOrThrow(attendanceCheckId); + attendanceCheck.increaseReportCount(); + attendanceCheckCommandPort.update(attendanceCheck); + + return AttendanceCheckReportResult.of(attendanceCheck.getId(), attendanceCheck.getReportCount()); + } +} diff --git a/src/main/java/konkuk/thip/roompost/application/service/RecordReportService.java b/src/main/java/konkuk/thip/roompost/application/service/RecordReportService.java new file mode 100644 index 000000000..ba8620bc1 --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/service/RecordReportService.java @@ -0,0 +1,26 @@ +package konkuk.thip.roompost.application.service; + +import konkuk.thip.roompost.application.port.in.RecordReportUseCase; +import konkuk.thip.roompost.application.port.in.dto.record.RecordReportResult; +import konkuk.thip.roompost.application.port.out.RecordCommandPort; +import konkuk.thip.roompost.domain.Record; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class RecordReportService implements RecordReportUseCase { + + private final RecordCommandPort recordCommandPort; + + @Override + @Transactional + public RecordReportResult reportRecord(Long recordId) { + Record record = recordCommandPort.getByIdOrThrow(recordId); + record.increaseReportCount(); + recordCommandPort.update(record); + + return RecordReportResult.of(record.getId(), record.getReportCount()); + } +} diff --git a/src/main/java/konkuk/thip/roompost/application/service/VoteReportService.java b/src/main/java/konkuk/thip/roompost/application/service/VoteReportService.java new file mode 100644 index 000000000..c8525b7bc --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteReportService.java @@ -0,0 +1,26 @@ +package konkuk.thip.roompost.application.service; + +import konkuk.thip.roompost.application.port.in.VoteReportUseCase; +import konkuk.thip.roompost.application.port.in.dto.vote.VoteReportResult; +import konkuk.thip.roompost.application.port.out.VoteCommandPort; +import konkuk.thip.roompost.domain.Vote; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class VoteReportService implements VoteReportUseCase { + + private final VoteCommandPort voteCommandPort; + + @Override + @Transactional + public VoteReportResult reportVote(Long voteId) { + Vote vote = voteCommandPort.getByIdOrThrow(voteId); + vote.increaseReportCount(); + voteCommandPort.updateVote(vote); + + return VoteReportResult.of(vote.getId(), vote.getReportCount()); + } +} diff --git a/src/main/java/konkuk/thip/roompost/domain/AttendanceCheck.java b/src/main/java/konkuk/thip/roompost/domain/AttendanceCheck.java index de23f167d..98e0628e7 100644 --- a/src/main/java/konkuk/thip/roompost/domain/AttendanceCheck.java +++ b/src/main/java/konkuk/thip/roompost/domain/AttendanceCheck.java @@ -2,6 +2,7 @@ import konkuk.thip.common.entity.BaseDomainEntity; import konkuk.thip.common.exception.InvalidStateException; +import lombok.Builder; import lombok.Getter; import lombok.experimental.SuperBuilder; @@ -22,6 +23,9 @@ public class AttendanceCheck extends BaseDomainEntity { private Long creatorId; + @Builder.Default + private int reportCount = 0; + public static AttendanceCheck withoutId(Long roomId, Long creatorId, String todayComment, int alreadyWrittenCountTodayOfUser) { validateWriteCount(alreadyWrittenCountTodayOfUser); @@ -43,4 +47,8 @@ public void validateCreator(Long userId) { throw new InvalidStateException(ATTENDANCE_CHECK_CAN_NOT_DELETE); } } + + public void increaseReportCount() { + reportCount++; + } } diff --git a/src/main/java/konkuk/thip/roompost/domain/Record.java b/src/main/java/konkuk/thip/roompost/domain/Record.java index f62c314ac..8d74a745d 100644 --- a/src/main/java/konkuk/thip/roompost/domain/Record.java +++ b/src/main/java/konkuk/thip/roompost/domain/Record.java @@ -29,6 +29,9 @@ public class Record extends BaseDomainEntity implements RoomPost { @Builder.Default private Integer commentCount = 0; + @Builder.Default + private int reportCount = 0; + private Long roomId; public static Record withoutId( @@ -73,6 +76,10 @@ public void validatePage(int totalPageCount) { } } + public void increaseReportCount() { + reportCount++; + } + @Override public void increaseCommentCount() { commentCount++; diff --git a/src/main/java/konkuk/thip/roompost/domain/Vote.java b/src/main/java/konkuk/thip/roompost/domain/Vote.java index 3f3111193..c7eadb18d 100644 --- a/src/main/java/konkuk/thip/roompost/domain/Vote.java +++ b/src/main/java/konkuk/thip/roompost/domain/Vote.java @@ -29,6 +29,9 @@ public class Vote extends BaseDomainEntity implements RoomPost { @Builder.Default private Integer commentCount = 0; + @Builder.Default + private int reportCount = 0; + private Long roomId; public static Vote withoutId(String content, Long creatorId, Integer page, boolean isOverview, Long roomId) { @@ -67,6 +70,10 @@ public void validatePage(int totalPageCount) { } } + public void increaseReportCount() { + reportCount++; + } + @Override public void increaseCommentCount() { commentCount++; diff --git a/src/main/resources/db/migration/V260807__Add_report_count_to_rooms_and_attendance_checks.sql b/src/main/resources/db/migration/V260807__Add_report_count_to_rooms_and_attendance_checks.sql new file mode 100644 index 000000000..93a8e6dcc --- /dev/null +++ b/src/main/resources/db/migration/V260807__Add_report_count_to_rooms_and_attendance_checks.sql @@ -0,0 +1,12 @@ +ALTER TABLE rooms + ADD COLUMN report_count INT NOT NULL DEFAULT 0; + +ALTER TABLE attendance_checks + ADD COLUMN report_count INT NOT NULL DEFAULT 0; + +UPDATE posts +SET report_count = 0 +WHERE report_count IS NULL; + +ALTER TABLE posts + MODIFY COLUMN report_count INT NOT NULL DEFAULT 0; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomReportApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomReportApiTest.java new file mode 100644 index 000000000..5f9df034d --- /dev/null +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomReportApiTest.java @@ -0,0 +1,113 @@ +package konkuk.thip.room.adapter.in.web; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; +import konkuk.thip.room.domain.value.Category; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static konkuk.thip.common.exception.code.ErrorCode.ROOM_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 모임방 신고 api 통합 테스트") +class RoomReportApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private BookJpaRepository bookJpaRepository; + @Autowired private RoomJpaRepository roomJpaRepository; + @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; + + private UserJpaEntity user; + private RoomJpaEntity room; + + private static final String ROOM_REPORT_API_PATH = "/rooms/{roomId}/report"; + + @BeforeEach + void setUp() { + Alias alias = TestEntityFactory.createLiteratureAlias(); + user = userJpaRepository.save(TestEntityFactory.createUser(alias)); + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); + Category category = TestEntityFactory.createLiteratureCategory(); + room = roomJpaRepository.save(TestEntityFactory.createRoom(book, category)); + } + + @AfterEach + void tearDown() { + roomParticipantJpaRepository.deleteAllInBatch(); + roomJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("모임방을 신고하면 신고 횟수만 1 증가한다 [성공]") + void reportRoom_success() throws Exception { + // when + mockMvc.perform(post(ROOM_REPORT_API_PATH, room.getRoomId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.roomId").value(room.getRoomId())) + .andExpect(jsonPath("$.data.reportCount").value(1)); + + // then + RoomJpaEntity updatedRoom = roomJpaRepository.findById(room.getRoomId()).orElseThrow(); + assertThat(updatedRoom.getReportCount()).isEqualTo(1); + } + + @Test + @DisplayName("같은 모임방을 두 번 신고하면 신고 횟수가 누적된다 [성공]") + void reportRoom_twice_accumulates() throws Exception { + // when + mockMvc.perform(post(ROOM_REPORT_API_PATH, room.getRoomId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mockMvc.perform(post(ROOM_REPORT_API_PATH, room.getRoomId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.reportCount").value(2)); + + // then + RoomJpaEntity updatedRoom = roomJpaRepository.findById(room.getRoomId()).orElseThrow(); + assertThat(updatedRoom.getReportCount()).isEqualTo(2); + } + + @Test + @DisplayName("존재하지 않는 모임방을 신고하면 [404 에러 발생]") + void reportRoom_notFound_fail() throws Exception { + // given + long nonExistentRoomId = 999_999L; + + // when & then + mockMvc.perform(post(ROOM_REPORT_API_PATH, nonExistentRoomId) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(ROOM_NOT_FOUND.getCode())); + } +} diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckReportApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckReportApiTest.java new file mode 100644 index 000000000..72e6842ea --- /dev/null +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckReportApiTest.java @@ -0,0 +1,125 @@ +package konkuk.thip.roompost.adapter.in.web; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; +import konkuk.thip.room.domain.value.Category; +import konkuk.thip.roompost.adapter.out.jpa.AttendanceCheckJpaEntity; +import konkuk.thip.roompost.adapter.out.persistence.repository.attendancecheck.AttendanceCheckJpaRepository; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static konkuk.thip.common.exception.code.ErrorCode.ATTENDANCE_CHECK_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 오늘의 한마디 신고 api 통합 테스트") +class AttendanceCheckReportApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private BookJpaRepository bookJpaRepository; + @Autowired private RoomJpaRepository roomJpaRepository; + @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; + @Autowired private AttendanceCheckJpaRepository attendanceCheckJpaRepository; + @Autowired private NotificationJpaRepository notificationJpaRepository; + + private UserJpaEntity user; + private RoomJpaEntity room; + private AttendanceCheckJpaEntity attendanceCheck; + + private static final String ATTENDANCE_CHECK_REPORT_API_PATH = "/rooms/{roomId}/daily-greeting/{attendanceCheckId}/report"; + + @BeforeEach + void setUp() { + Alias alias = TestEntityFactory.createLiteratureAlias(); + user = userJpaRepository.save(TestEntityFactory.createUser(alias)); + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); + Category category = TestEntityFactory.createLiteratureCategory(); + room = roomJpaRepository.save(TestEntityFactory.createRoom(book, category)); + attendanceCheck = attendanceCheckJpaRepository.save( + TestEntityFactory.createAttendanceCheck("오늘의 한마디", room, user) + ); + } + + @AfterEach + void tearDown() { + attendanceCheckJpaRepository.deleteAllInBatch(); + roomParticipantJpaRepository.deleteAllInBatch(); + roomJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); + // 다른 테스트에서 생성된 알림이 정리되지 않은 채 남아 있으면 유저 삭제 시 FK 위반이 발생하므로 함께 정리 + notificationJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("오늘의 한마디를 신고하면 신고 횟수만 1 증가한다 [성공]") + void reportAttendanceCheck_success() throws Exception { + // when + mockMvc.perform(post(ATTENDANCE_CHECK_REPORT_API_PATH, room.getRoomId(), attendanceCheck.getAttendanceCheckId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.attendanceCheckId").value(attendanceCheck.getAttendanceCheckId())) + .andExpect(jsonPath("$.data.reportCount").value(1)); + + // then + AttendanceCheckJpaEntity updated = attendanceCheckJpaRepository.findByAttendanceCheckId(attendanceCheck.getAttendanceCheckId()).orElseThrow(); + assertThat(updated.getReportCount()).isEqualTo(1); + } + + @Test + @DisplayName("같은 오늘의 한마디를 두 번 신고하면 신고 횟수가 누적된다 [성공]") + void reportAttendanceCheck_twice_accumulates() throws Exception { + // when + mockMvc.perform(post(ATTENDANCE_CHECK_REPORT_API_PATH, room.getRoomId(), attendanceCheck.getAttendanceCheckId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mockMvc.perform(post(ATTENDANCE_CHECK_REPORT_API_PATH, room.getRoomId(), attendanceCheck.getAttendanceCheckId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.reportCount").value(2)); + + // then + AttendanceCheckJpaEntity updated = attendanceCheckJpaRepository.findByAttendanceCheckId(attendanceCheck.getAttendanceCheckId()).orElseThrow(); + assertThat(updated.getReportCount()).isEqualTo(2); + } + + @Test + @DisplayName("존재하지 않는 오늘의 한마디를 신고하면 [404 에러 발생]") + void reportAttendanceCheck_notFound_fail() throws Exception { + // given + long nonExistentAttendanceCheckId = 999_999L; + + // when & then + mockMvc.perform(post(ATTENDANCE_CHECK_REPORT_API_PATH, room.getRoomId(), nonExistentAttendanceCheckId) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(ATTENDANCE_CHECK_NOT_FOUND.getCode())); + } +} diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordReportApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordReportApiTest.java new file mode 100644 index 000000000..c49cae6bf --- /dev/null +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordReportApiTest.java @@ -0,0 +1,119 @@ +package konkuk.thip.roompost.adapter.in.web; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; +import konkuk.thip.room.domain.value.Category; +import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; +import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static konkuk.thip.common.exception.code.ErrorCode.RECORD_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 기록 신고 api 통합 테스트") +class RecordReportApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private BookJpaRepository bookJpaRepository; + @Autowired private RoomJpaRepository roomJpaRepository; + @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; + @Autowired private RecordJpaRepository recordJpaRepository; + + private UserJpaEntity user; + private RoomJpaEntity room; + private RecordJpaEntity record; + + private static final String RECORD_REPORT_API_PATH = "/rooms/{roomId}/record/{recordId}/report"; + + @BeforeEach + void setUp() { + Alias alias = TestEntityFactory.createLiteratureAlias(); + user = userJpaRepository.save(TestEntityFactory.createUser(alias)); + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); + Category category = TestEntityFactory.createLiteratureCategory(); + room = roomJpaRepository.save(TestEntityFactory.createRoom(book, category)); + record = recordJpaRepository.save(TestEntityFactory.createRecord(user, room)); + } + + @AfterEach + void tearDown() { + recordJpaRepository.deleteAllInBatch(); + roomParticipantJpaRepository.deleteAllInBatch(); + roomJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("기록을 신고하면 신고 횟수만 1 증가한다 [성공]") + void reportRecord_success() throws Exception { + // when + mockMvc.perform(post(RECORD_REPORT_API_PATH, room.getRoomId(), record.getPostId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.recordId").value(record.getPostId())) + .andExpect(jsonPath("$.data.reportCount").value(1)); + + // then + RecordJpaEntity updatedRecord = recordJpaRepository.findById(record.getPostId()).orElseThrow(); + assertThat(updatedRecord.getReportCount()).isEqualTo(1); + } + + @Test + @DisplayName("같은 기록을 두 번 신고하면 신고 횟수가 누적된다 [성공]") + void reportRecord_twice_accumulates() throws Exception { + // when + mockMvc.perform(post(RECORD_REPORT_API_PATH, room.getRoomId(), record.getPostId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mockMvc.perform(post(RECORD_REPORT_API_PATH, room.getRoomId(), record.getPostId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.reportCount").value(2)); + + // then + RecordJpaEntity updatedRecord = recordJpaRepository.findById(record.getPostId()).orElseThrow(); + assertThat(updatedRecord.getReportCount()).isEqualTo(2); + } + + @Test + @DisplayName("존재하지 않는 기록을 신고하면 [404 에러 발생]") + void reportRecord_notFound_fail() throws Exception { + // given + long nonExistentRecordId = 999_999L; + + // when & then + mockMvc.perform(post(RECORD_REPORT_API_PATH, room.getRoomId(), nonExistentRecordId) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(RECORD_NOT_FOUND.getCode())); + } +} diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteReportApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteReportApiTest.java new file mode 100644 index 000000000..bb4618fcb --- /dev/null +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteReportApiTest.java @@ -0,0 +1,119 @@ +package konkuk.thip.roompost.adapter.in.web; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; +import konkuk.thip.room.domain.value.Category; +import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; +import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static konkuk.thip.common.exception.code.ErrorCode.VOTE_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 투표 신고 api 통합 테스트") +class VoteReportApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private BookJpaRepository bookJpaRepository; + @Autowired private RoomJpaRepository roomJpaRepository; + @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; + @Autowired private VoteJpaRepository voteJpaRepository; + + private UserJpaEntity user; + private RoomJpaEntity room; + private VoteJpaEntity vote; + + private static final String VOTE_REPORT_API_PATH = "/rooms/{roomId}/vote/{voteId}/report"; + + @BeforeEach + void setUp() { + Alias alias = TestEntityFactory.createLiteratureAlias(); + user = userJpaRepository.save(TestEntityFactory.createUser(alias)); + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); + Category category = TestEntityFactory.createLiteratureCategory(); + room = roomJpaRepository.save(TestEntityFactory.createRoom(book, category)); + vote = voteJpaRepository.save(TestEntityFactory.createVote(user, room)); + } + + @AfterEach + void tearDown() { + voteJpaRepository.deleteAllInBatch(); + roomParticipantJpaRepository.deleteAllInBatch(); + roomJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("투표를 신고하면 신고 횟수만 1 증가한다 [성공]") + void reportVote_success() throws Exception { + // when + mockMvc.perform(post(VOTE_REPORT_API_PATH, room.getRoomId(), vote.getPostId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.voteId").value(vote.getPostId())) + .andExpect(jsonPath("$.data.reportCount").value(1)); + + // then + VoteJpaEntity updatedVote = voteJpaRepository.findById(vote.getPostId()).orElseThrow(); + assertThat(updatedVote.getReportCount()).isEqualTo(1); + } + + @Test + @DisplayName("같은 투표를 두 번 신고하면 신고 횟수가 누적된다 [성공]") + void reportVote_twice_accumulates() throws Exception { + // when + mockMvc.perform(post(VOTE_REPORT_API_PATH, room.getRoomId(), vote.getPostId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mockMvc.perform(post(VOTE_REPORT_API_PATH, room.getRoomId(), vote.getPostId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.reportCount").value(2)); + + // then + VoteJpaEntity updatedVote = voteJpaRepository.findById(vote.getPostId()).orElseThrow(); + assertThat(updatedVote.getReportCount()).isEqualTo(2); + } + + @Test + @DisplayName("존재하지 않는 투표를 신고하면 [404 에러 발생]") + void reportVote_notFound_fail() throws Exception { + // given + long nonExistentVoteId = 999_999L; + + // when & then + mockMvc.perform(post(VOTE_REPORT_API_PATH, room.getRoomId(), nonExistentVoteId) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(VOTE_NOT_FOUND.getCode())); + } +}