From 730b3437f6960110c60f5dee4996c49304de323a Mon Sep 17 00:00:00 2001 From: sehi55 Date: Thu, 6 Aug 2026 16:59:28 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=EC=9E=90=20profileId=20=EC=9D=91=EB=8B=B5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/meeting/dto/response/MatchedChatRoomInfo.java | 1 + .../domain/chat/repository/ChatMemberQueryRepository.java | 1 + 2 files changed, 2 insertions(+) diff --git a/manabom/src/main/java/mannabom_server/manabom/application/meeting/dto/response/MatchedChatRoomInfo.java b/manabom/src/main/java/mannabom_server/manabom/application/meeting/dto/response/MatchedChatRoomInfo.java index 8b66d4d..ea3ae2e 100644 --- a/manabom/src/main/java/mannabom_server/manabom/application/meeting/dto/response/MatchedChatRoomInfo.java +++ b/manabom/src/main/java/mannabom_server/manabom/application/meeting/dto/response/MatchedChatRoomInfo.java @@ -27,6 +27,7 @@ public static MatchedChatRoomInfo of(Long roomId, List participants public record Participant( Long userId, + Long profileId, Gender gender, String nickname, String profileImg diff --git a/manabom/src/main/java/mannabom_server/manabom/domain/chat/repository/ChatMemberQueryRepository.java b/manabom/src/main/java/mannabom_server/manabom/domain/chat/repository/ChatMemberQueryRepository.java index e811200..3d90c01 100644 --- a/manabom/src/main/java/mannabom_server/manabom/domain/chat/repository/ChatMemberQueryRepository.java +++ b/manabom/src/main/java/mannabom_server/manabom/domain/chat/repository/ChatMemberQueryRepository.java @@ -16,6 +16,7 @@ public List findParticipantsByRoomId(Long roomI return em.createQuery(""" select new mannabom_server.manabom.application.meeting.dto.response.MatchedChatRoomInfo$Participant( u.userId, + p.profileId, p.gender, p.nickName, ( From d5966c170c0cba7bc4f40100059b27e9a18d2d07 Mon Sep 17 00:00:00 2001 From: sehi55 Date: Thu, 6 Aug 2026 16:59:39 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EC=99=B8=EB=B6=80=20Long=20?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20=EB=AC=B8=EC=9E=90=EC=97=B4=EB=A1=9C=20?= =?UTF-8?q?=EC=A7=81=EB=A0=AC=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/config/JacksonConfig.java | 18 +++++ .../config/JacksonConfigTest.java | 79 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 manabom/src/main/java/mannabom_server/manabom/infrastructure/config/JacksonConfig.java create mode 100644 manabom/src/test/java/mannabom_server/manabom/infrastructure/config/JacksonConfigTest.java diff --git a/manabom/src/main/java/mannabom_server/manabom/infrastructure/config/JacksonConfig.java b/manabom/src/main/java/mannabom_server/manabom/infrastructure/config/JacksonConfig.java new file mode 100644 index 0000000..a4ae401 --- /dev/null +++ b/manabom/src/main/java/mannabom_server/manabom/infrastructure/config/JacksonConfig.java @@ -0,0 +1,18 @@ +package mannabom_server.manabom.infrastructure.config; + +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfig { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer longAsStringCustomizer() { + return builder -> builder.serializerByType( + Long.class, + ToStringSerializer.instance + ); + } +} diff --git a/manabom/src/test/java/mannabom_server/manabom/infrastructure/config/JacksonConfigTest.java b/manabom/src/test/java/mannabom_server/manabom/infrastructure/config/JacksonConfigTest.java new file mode 100644 index 0000000..cb26c62 --- /dev/null +++ b/manabom/src/test/java/mannabom_server/manabom/infrastructure/config/JacksonConfigTest.java @@ -0,0 +1,79 @@ +package mannabom_server.manabom.infrastructure.config; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import mannabom_server.manabom.application.meeting.dto.response.MatchedChatRoomInfo; +import mannabom_server.manabom.domain.user.enums.Gender; +import org.junit.jupiter.api.Test; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class JacksonConfigTest { + + private final ObjectMapper objectMapper = objectMapper(); + + @Test + void serializesExternalLongIdsAsStrings() throws Exception { + ExternalPayload payload = new ExternalPayload( + 10L, + List.of(20L, 21L), + Map.of("roomId", 30L), + 2L + ); + + JsonNode json = objectMapper.readTree(objectMapper.writeValueAsString(payload)); + + assertThat(json.get("userId").asText()).isEqualTo("10"); + assertThat(json.get("recipientUserIds").get(0).asText()).isEqualTo("20"); + assertThat(json.get("data").get("roomId").asText()).isEqualTo("30"); + assertThat(json.get("totalCount").isIntegralNumber()).isTrue(); + } + + @Test + void includesProfileIdInMatchedChatParticipant() throws Exception { + MatchedChatRoomInfo.Participant participant = + new MatchedChatRoomInfo.Participant( + 10L, + 100L, + Gender.MALE, + "봄이", + "profile.jpg" + ); + + JsonNode json = objectMapper.readTree(objectMapper.writeValueAsString(participant)); + + assertThat(json.get("userId").asText()).isEqualTo("10"); + assertThat(json.get("profileId").asText()).isEqualTo("100"); + } + + @Test + void acceptsStringIdInIncomingJson() throws Exception { + IncomingPayload payload = objectMapper.readValue( + "{\"targetId\":\"10\"}", + IncomingPayload.class + ); + + assertThat(payload.targetId()).isEqualTo(10L); + } + + private ObjectMapper objectMapper() { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); + new JacksonConfig().longAsStringCustomizer().customize(builder); + return builder.build(); + } + + private record ExternalPayload( + Long userId, + List recipientUserIds, + Map data, + long totalCount + ) { + } + + private record IncomingPayload(Long targetId) { + } +} From 28b90114d574c11f55a135cb3028016b8ef4ecdd Mon Sep 17 00:00:00 2001 From: sehi55 Date: Thu, 6 Aug 2026 17:00:21 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EC=8B=A0=EA=B3=A0=20API=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=EA=B3=BC=20=EC=A4=91=EB=B3=B5=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/CreateChatReportRequest.java | 28 ++++ .../request/CreateProfileReportRequest.java | 25 ++++ .../dto/request/CreateReportRequest.java | 12 -- .../dto/response/CreateReportResponse.java | 8 ++ .../report/service/ReportService.java | 101 +++++++++---- .../report/repository/ReportRepository.java | 11 ++ .../user/repository/UserRepository.java | 7 + .../report/controller/ReportController.java | 30 ++-- .../request/ReportRequestValidationTest.java | 39 +++++ .../report/service/ReportServiceTest.java | 136 ++++++++++++++++++ .../controller/ReportControllerTest.java | 33 +++++ 11 files changed, 384 insertions(+), 46 deletions(-) create mode 100644 manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateChatReportRequest.java create mode 100644 manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateProfileReportRequest.java delete mode 100644 manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateReportRequest.java create mode 100644 manabom/src/main/java/mannabom_server/manabom/application/report/dto/response/CreateReportResponse.java create mode 100644 manabom/src/test/java/mannabom_server/manabom/application/report/dto/request/ReportRequestValidationTest.java create mode 100644 manabom/src/test/java/mannabom_server/manabom/application/report/service/ReportServiceTest.java create mode 100644 manabom/src/test/java/mannabom_server/manabom/presentation/report/controller/ReportControllerTest.java diff --git a/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateChatReportRequest.java b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateChatReportRequest.java new file mode 100644 index 0000000..92d4f6c --- /dev/null +++ b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateChatReportRequest.java @@ -0,0 +1,28 @@ +package mannabom_server.manabom.application.report.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mannabom_server.manabom.domain.report.entity.ReportReason; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateChatReportRequest { + + @NotNull(message = "채팅방 ID는 필수입니다.") + private Long contextId; + + @NotNull(message = "신고 대상 사용자 ID는 필수입니다.") + private Long targetId; + + @NotNull(message = "신고 사유는 필수입니다.") + private ReportReason reason; + + @Size(max = 1000, message = "신고 상세 내용은 최대 1000자까지 입력할 수 있습니다.") + private String additionalDetail; +} diff --git a/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateProfileReportRequest.java b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateProfileReportRequest.java new file mode 100644 index 0000000..4eb72e5 --- /dev/null +++ b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateProfileReportRequest.java @@ -0,0 +1,25 @@ +package mannabom_server.manabom.application.report.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mannabom_server.manabom.domain.report.entity.ReportReason; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateProfileReportRequest { + + @NotNull(message = "신고 대상 프로필 ID는 필수입니다.") + private Long profileId; + + @NotNull(message = "신고 사유는 필수입니다.") + private ReportReason reason; + + @Size(max = 1000, message = "신고 상세 내용은 최대 1000자까지 입력할 수 있습니다.") + private String additionalDetail; +} diff --git a/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateReportRequest.java b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateReportRequest.java deleted file mode 100644 index 1ea470f..0000000 --- a/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateReportRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package mannabom_server.manabom.application.report.dto.request; - -import lombok.Getter; -import mannabom_server.manabom.domain.report.entity.ReportReason; - -@Getter -public class CreateReportRequest { - private Long contextId; - private Long targetId; - private ReportReason reason; - private String additionalDetail; -} diff --git a/manabom/src/main/java/mannabom_server/manabom/application/report/dto/response/CreateReportResponse.java b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/response/CreateReportResponse.java new file mode 100644 index 0000000..fd8734c --- /dev/null +++ b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/response/CreateReportResponse.java @@ -0,0 +1,8 @@ +package mannabom_server.manabom.application.report.dto.response; + +public record CreateReportResponse(Long reportId) { + + public static CreateReportResponse of(Long reportId) { + return new CreateReportResponse(reportId); + } +} diff --git a/manabom/src/main/java/mannabom_server/manabom/application/report/service/ReportService.java b/manabom/src/main/java/mannabom_server/manabom/application/report/service/ReportService.java index 9f60e88..7a45b58 100644 --- a/manabom/src/main/java/mannabom_server/manabom/application/report/service/ReportService.java +++ b/manabom/src/main/java/mannabom_server/manabom/application/report/service/ReportService.java @@ -1,53 +1,111 @@ package mannabom_server.manabom.application.report.service; import lombok.RequiredArgsConstructor; -import mannabom_server.manabom.application.report.dto.request.CreateReportRequest; +import mannabom_server.manabom.application.report.dto.request.CreateChatReportRequest; +import mannabom_server.manabom.application.report.dto.request.CreateProfileReportRequest; import mannabom_server.manabom.domain.chat.repository.ChatMemberRepository; import mannabom_server.manabom.domain.chat.repository.ChatRoomRepository; import mannabom_server.manabom.domain.report.entity.Report; +import mannabom_server.manabom.domain.report.entity.ReportReason; +import mannabom_server.manabom.domain.report.entity.ReportStatus; import mannabom_server.manabom.domain.report.entity.ReportType; import mannabom_server.manabom.domain.report.repository.ReportRepository; +import mannabom_server.manabom.domain.user.entity.Profile; import mannabom_server.manabom.domain.user.entity.User; +import mannabom_server.manabom.domain.user.repository.ProfileRepository; import mannabom_server.manabom.domain.user.repository.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @RequiredArgsConstructor public class ReportService { + private static final List ACTIVE_REPORT_STATUSES = + List.of(ReportStatus.RECEIVED, ReportStatus.UNDER_REVIEW); + private final ReportRepository reportRepository; private final UserRepository userRepository; + private final ProfileRepository profileRepository; private final ChatRoomRepository chatRoomRepository; private final ChatMemberRepository chatMemberRepository; @Transactional - public void createReport(Long userId, CreateReportRequest request, ReportType type) { - if (userId.equals(request.getTargetId())) - throw new IllegalArgumentException("자기 자신을 신고할 수 없습니다."); - User user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 입니다.")); + public Long createChatReport(Long userId, CreateChatReportRequest request) { + User reporter = findReporterWithLock(userId); User target = userRepository.findById(request.getTargetId()) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 타겟 유저 입니다.")); + validateNotSelfReport(reporter, target); + validateChatReport(userId, request.getTargetId(), request.getContextId()); - if (type == ReportType.CHAT) { - validateChatReport(userId, request.getTargetId(), request.getContextId()); - } - - Report report = Report.createReport( - user, + return findOrCreateReport( + reporter, target, - type, + ReportType.CHAT, request.getContextId(), request.getReason(), request.getAdditionalDetail() ); - reportRepository.save(report); } - private void validateChatReport(Long reporterId, Long targetId, Long roomId) { - if (roomId == null) { - throw new IllegalArgumentException("채팅 신고에는 채팅방 ID가 필요합니다."); + @Transactional + public Long createProfileReport(Long userId, CreateProfileReportRequest request) { + User reporter = findReporterWithLock(userId); + Profile targetProfile = profileRepository.findById(request.getProfileId()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 타겟 프로필입니다.")); + User target = targetProfile.getUser(); + validateNotSelfReport(reporter, target); + + return findOrCreateReport( + reporter, + target, + ReportType.PROFILE, + request.getProfileId(), + request.getReason(), + request.getAdditionalDetail() + ); + } + + private User findReporterWithLock(Long userId) { + return userRepository.findByIdForUpdate(userId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 입니다.")); + } + + private void validateNotSelfReport(User reporter, User target) { + if (reporter.getUserId().equals(target.getUserId())) { + throw new IllegalArgumentException("자기 자신을 신고할 수 없습니다."); } + } + + private Long findOrCreateReport( + User reporter, + User target, + ReportType type, + Long contextId, + ReportReason reason, + String additionalDetail + ) { + return reportRepository + .findFirstByReporter_UserIdAndTarget_UserIdAndTypeAndContextIdAndStatusInOrderByIdAsc( + reporter.getUserId(), + target.getUserId(), + type, + contextId, + ACTIVE_REPORT_STATUSES + ) + .map(Report::getId) + .orElseGet(() -> reportRepository.save(Report.createReport( + reporter, + target, + type, + contextId, + reason, + additionalDetail + )).getId()); + } + + private void validateChatReport(Long reporterId, Long targetId, Long roomId) { if (!chatRoomRepository.existsById(roomId)) { throw new IllegalArgumentException("존재하지 않는 채팅방입니다."); } @@ -58,13 +116,4 @@ private void validateChatReport(Long reporterId, Long targetId, Long roomId) { throw new IllegalArgumentException("신고 대상자가 해당 채팅방의 참여자가 아닙니다."); } } - - //특정 유저 신고 내역 조회 - //처리해야할 신고 리스트 조회 - //처리된 신고 리스트 조회 - //전체 신고 리스트 조회? - //신고 처리하기 - - - } diff --git a/manabom/src/main/java/mannabom_server/manabom/domain/report/repository/ReportRepository.java b/manabom/src/main/java/mannabom_server/manabom/domain/report/repository/ReportRepository.java index 68d8c29..f44ed37 100644 --- a/manabom/src/main/java/mannabom_server/manabom/domain/report/repository/ReportRepository.java +++ b/manabom/src/main/java/mannabom_server/manabom/domain/report/repository/ReportRepository.java @@ -1,13 +1,24 @@ package mannabom_server.manabom.domain.report.repository; import mannabom_server.manabom.domain.report.entity.Report; +import mannabom_server.manabom.domain.report.entity.ReportStatus; +import mannabom_server.manabom.domain.report.entity.ReportType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.Collection; import java.util.Optional; public interface ReportRepository extends JpaRepository { + Optional findFirstByReporter_UserIdAndTarget_UserIdAndTypeAndContextIdAndStatusInOrderByIdAsc( + Long reporterId, + Long targetId, + ReportType type, + Long contextId, + Collection statuses + ); + @Query(""" select r from Report r diff --git a/manabom/src/main/java/mannabom_server/manabom/domain/user/repository/UserRepository.java b/manabom/src/main/java/mannabom_server/manabom/domain/user/repository/UserRepository.java index d9bd0e3..7d39fc2 100644 --- a/manabom/src/main/java/mannabom_server/manabom/domain/user/repository/UserRepository.java +++ b/manabom/src/main/java/mannabom_server/manabom/domain/user/repository/UserRepository.java @@ -1,8 +1,11 @@ package mannabom_server.manabom.domain.user.repository; +import jakarta.persistence.LockModeType; import mannabom_server.manabom.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -10,6 +13,10 @@ @Repository public interface UserRepository extends JpaRepository { + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select u from User u where u.userId = :userId") + Optional findByIdForUpdate(@Param("userId") Long userId); + /** * 카카오 ID로 사용자 조회 */ diff --git a/manabom/src/main/java/mannabom_server/manabom/presentation/report/controller/ReportController.java b/manabom/src/main/java/mannabom_server/manabom/presentation/report/controller/ReportController.java index 42393ad..2d48d61 100644 --- a/manabom/src/main/java/mannabom_server/manabom/presentation/report/controller/ReportController.java +++ b/manabom/src/main/java/mannabom_server/manabom/presentation/report/controller/ReportController.java @@ -1,10 +1,12 @@ package mannabom_server.manabom.presentation.report.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mannabom_server.manabom.application.common.dto.ApiResponse; -import mannabom_server.manabom.application.report.dto.request.CreateReportRequest; +import mannabom_server.manabom.application.report.dto.request.CreateChatReportRequest; +import mannabom_server.manabom.application.report.dto.request.CreateProfileReportRequest; +import mannabom_server.manabom.application.report.dto.response.CreateReportResponse; import mannabom_server.manabom.application.report.service.ReportService; -import mannabom_server.manabom.domain.report.entity.ReportType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; @@ -19,15 +21,27 @@ public class ReportController { private final ReportService reportService; @PostMapping("/chat") - public ResponseEntity> reportChat(@RequestBody CreateReportRequest request, @AuthenticationPrincipal Long userId){ - reportService.createReport(userId, request, ReportType.CHAT); - return ResponseEntity.ok(ApiResponse.success(null, "채팅 신고를 완료했습니다.")); + public ResponseEntity> reportChat( + @Valid @RequestBody CreateChatReportRequest request, + @AuthenticationPrincipal Long userId + ) { + Long reportId = reportService.createChatReport(userId, request); + return ResponseEntity.ok(ApiResponse.success( + CreateReportResponse.of(reportId), + "채팅 신고를 완료했습니다." + )); } @PostMapping("/profile") - public ResponseEntity> reportProfile(@RequestBody CreateReportRequest request, @AuthenticationPrincipal Long userId){ - reportService.createReport(userId, request, ReportType.PROFILE); - return ResponseEntity.ok(ApiResponse.success(null, "프로필 신고를 완료했습니다.")); + public ResponseEntity> reportProfile( + @Valid @RequestBody CreateProfileReportRequest request, + @AuthenticationPrincipal Long userId + ) { + Long reportId = reportService.createProfileReport(userId, request); + return ResponseEntity.ok(ApiResponse.success( + CreateReportResponse.of(reportId), + "프로필 신고를 완료했습니다." + )); } } diff --git a/manabom/src/test/java/mannabom_server/manabom/application/report/dto/request/ReportRequestValidationTest.java b/manabom/src/test/java/mannabom_server/manabom/application/report/dto/request/ReportRequestValidationTest.java new file mode 100644 index 0000000..effaadd --- /dev/null +++ b/manabom/src/test/java/mannabom_server/manabom/application/report/dto/request/ReportRequestValidationTest.java @@ -0,0 +1,39 @@ +package mannabom_server.manabom.application.report.dto.request; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import mannabom_server.manabom.domain.report.entity.ReportReason; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ReportRequestValidationTest { + + private final Validator validator = Validation + .buildDefaultValidatorFactory() + .getValidator(); + + @Test + void rejectsChatReportDetailLongerThanOneThousandCharacters() { + CreateChatReportRequest request = CreateChatReportRequest.builder() + .contextId(100L) + .targetId(2L) + .reason(ReportReason.ETC) + .additionalDetail("a".repeat(1001)) + .build(); + + assertThat(validator.validate(request)) + .anyMatch(violation -> violation.getPropertyPath().toString() + .equals("additionalDetail")); + } + + @Test + void allowsProfileReportWithoutAdditionalDetail() { + CreateProfileReportRequest request = CreateProfileReportRequest.builder() + .profileId(200L) + .reason(ReportReason.INAPPROPRIATE_PROFILE) + .build(); + + assertThat(validator.validate(request)).isEmpty(); + } +} diff --git a/manabom/src/test/java/mannabom_server/manabom/application/report/service/ReportServiceTest.java b/manabom/src/test/java/mannabom_server/manabom/application/report/service/ReportServiceTest.java new file mode 100644 index 0000000..9ca11d8 --- /dev/null +++ b/manabom/src/test/java/mannabom_server/manabom/application/report/service/ReportServiceTest.java @@ -0,0 +1,136 @@ +package mannabom_server.manabom.application.report.service; + +import mannabom_server.manabom.application.report.dto.request.CreateChatReportRequest; +import mannabom_server.manabom.application.report.dto.request.CreateProfileReportRequest; +import mannabom_server.manabom.domain.chat.repository.ChatMemberRepository; +import mannabom_server.manabom.domain.chat.repository.ChatRoomRepository; +import mannabom_server.manabom.domain.report.entity.Report; +import mannabom_server.manabom.domain.report.entity.ReportReason; +import mannabom_server.manabom.domain.report.entity.ReportStatus; +import mannabom_server.manabom.domain.report.entity.ReportType; +import mannabom_server.manabom.domain.report.repository.ReportRepository; +import mannabom_server.manabom.domain.user.entity.Profile; +import mannabom_server.manabom.domain.user.entity.User; +import mannabom_server.manabom.domain.user.repository.ProfileRepository; +import mannabom_server.manabom.domain.user.repository.UserRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ReportServiceTest { + + @Mock private ReportRepository reportRepository; + @Mock private UserRepository userRepository; + @Mock private ProfileRepository profileRepository; + @Mock private ChatRoomRepository chatRoomRepository; + @Mock private ChatMemberRepository chatMemberRepository; + @InjectMocks private ReportService reportService; + + @Test + void createsProfileReportUsingProfileIdAsContextAndReturnsId() { + User reporter = user(1L); + User target = user(2L); + Profile targetProfile = Profile.builder().user(target).build(); + targetProfile.setProfileId(200L); + CreateProfileReportRequest request = CreateProfileReportRequest.builder() + .profileId(200L) + .reason(ReportReason.INAPPROPRIATE_PROFILE) + .additionalDetail("부적절한 소개입니다.") + .build(); + + when(userRepository.findByIdForUpdate(1L)).thenReturn(Optional.of(reporter)); + when(profileRepository.findById(200L)).thenReturn(Optional.of(targetProfile)); + when(reportRepository + .findFirstByReporter_UserIdAndTarget_UserIdAndTypeAndContextIdAndStatusInOrderByIdAsc( + eq(1L), + eq(2L), + eq(ReportType.PROFILE), + eq(200L), + anyCollection() + )).thenReturn(Optional.empty()); + when(reportRepository.save(any(Report.class))).thenAnswer(invocation -> { + Report report = invocation.getArgument(0); + ReflectionTestUtils.setField(report, "id", 300L); + return report; + }); + + Long reportId = reportService.createProfileReport(1L, request); + + assertThat(reportId).isEqualTo(300L); + ArgumentCaptor reportCaptor = ArgumentCaptor.forClass(Report.class); + verify(reportRepository).save(reportCaptor.capture()); + assertThat(reportCaptor.getValue().getTarget()).isEqualTo(target); + assertThat(reportCaptor.getValue().getType()).isEqualTo(ReportType.PROFILE); + assertThat(reportCaptor.getValue().getContextId()).isEqualTo(200L); + } + + @Test + void returnsExistingActiveReportIdForDuplicateChatReport() { + User reporter = user(1L); + User target = user(2L); + Report existing = Report.createReport( + reporter, + target, + ReportType.CHAT, + 100L, + ReportReason.ABUSIVE_LANGUAGE, + null + ); + ReflectionTestUtils.setField(existing, "id", 300L); + CreateChatReportRequest request = CreateChatReportRequest.builder() + .contextId(100L) + .targetId(2L) + .reason(ReportReason.ABUSIVE_LANGUAGE) + .build(); + + when(userRepository.findByIdForUpdate(1L)).thenReturn(Optional.of(reporter)); + when(userRepository.findById(2L)).thenReturn(Optional.of(target)); + when(chatRoomRepository.existsById(100L)).thenReturn(true); + when(chatMemberRepository.existsByRoomIdAndUser_UserId(100L, 1L)).thenReturn(true); + when(chatMemberRepository.existsByRoomIdAndUser_UserId(100L, 2L)).thenReturn(true); + when(reportRepository + .findFirstByReporter_UserIdAndTarget_UserIdAndTypeAndContextIdAndStatusInOrderByIdAsc( + eq(1L), + eq(2L), + eq(ReportType.CHAT), + eq(100L), + anyCollection() + )).thenReturn(Optional.of(existing)); + + Long reportId = reportService.createChatReport(1L, request); + + assertThat(reportId).isEqualTo(300L); + verify(reportRepository, never()).save(any(Report.class)); + verify(reportRepository) + .findFirstByReporter_UserIdAndTarget_UserIdAndTypeAndContextIdAndStatusInOrderByIdAsc( + 1L, + 2L, + ReportType.CHAT, + 100L, + java.util.List.of(ReportStatus.RECEIVED, ReportStatus.UNDER_REVIEW) + ); + } + + private User user(Long userId) { + return User.builder() + .userId(userId) + .kakaoId("kakao_" + userId) + .userName("user" + userId) + .build(); + } +} diff --git a/manabom/src/test/java/mannabom_server/manabom/presentation/report/controller/ReportControllerTest.java b/manabom/src/test/java/mannabom_server/manabom/presentation/report/controller/ReportControllerTest.java new file mode 100644 index 0000000..075c63b --- /dev/null +++ b/manabom/src/test/java/mannabom_server/manabom/presentation/report/controller/ReportControllerTest.java @@ -0,0 +1,33 @@ +package mannabom_server.manabom.presentation.report.controller; + +import mannabom_server.manabom.application.common.dto.ApiResponse; +import mannabom_server.manabom.application.report.dto.request.CreateProfileReportRequest; +import mannabom_server.manabom.application.report.dto.response.CreateReportResponse; +import mannabom_server.manabom.application.report.service.ReportService; +import org.junit.jupiter.api.Test; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ReportControllerTest { + + private final ReportService reportService = mock(ReportService.class); + private final ReportController reportController = new ReportController(reportService); + + @Test + void returnsCreatedReportIdForProfileReport() { + CreateProfileReportRequest request = CreateProfileReportRequest.builder() + .profileId(200L) + .build(); + when(reportService.createProfileReport(1L, request)).thenReturn(300L); + + ResponseEntity> response = + reportController.reportProfile(request, 1L); + + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getData().reportId()).isEqualTo(300L); + } +} From 9555d1a927482b084f38fbd74514bbf48fe77d17 Mon Sep 17 00:00:00 2001 From: sehi55 Date: Thu, 6 Aug 2026 18:47:28 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=EC=9D=8C=EC=88=98=20validation=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/dto/request/CreateChatReportRequest.java | 3 +++ .../report/dto/request/CreateProfileReportRequest.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateChatReportRequest.java b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateChatReportRequest.java index 92d4f6c..f4f0cec 100644 --- a/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateChatReportRequest.java +++ b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateChatReportRequest.java @@ -1,6 +1,7 @@ package mannabom_server.manabom.application.report.dto.request; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; @@ -15,9 +16,11 @@ public class CreateChatReportRequest { @NotNull(message = "채팅방 ID는 필수입니다.") + @Positive(message = "채팅방 ID는 0보다 커야 합니다.") private Long contextId; @NotNull(message = "신고 대상 사용자 ID는 필수입니다.") + @Positive(message = "신고 대상 사용자 ID는 0보다 커야 합니다.") private Long targetId; @NotNull(message = "신고 사유는 필수입니다.") diff --git a/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateProfileReportRequest.java b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateProfileReportRequest.java index 4eb72e5..62c4ede 100644 --- a/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateProfileReportRequest.java +++ b/manabom/src/main/java/mannabom_server/manabom/application/report/dto/request/CreateProfileReportRequest.java @@ -1,6 +1,7 @@ package mannabom_server.manabom.application.report.dto.request; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; @@ -15,6 +16,7 @@ public class CreateProfileReportRequest { @NotNull(message = "신고 대상 프로필 ID는 필수입니다.") + @Positive(message = "신고 대상 프로필 ID는 0보다 커야 합니다.") private Long profileId; @NotNull(message = "신고 사유는 필수입니다.") From 65abd0262d5b6a9f0360a2bcfbc1e2184b34371e Mon Sep 17 00:00:00 2001 From: sehi55 Date: Thu, 6 Aug 2026 18:47:37 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manabom/infrastructure/config/JacksonConfigTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/manabom/src/test/java/mannabom_server/manabom/infrastructure/config/JacksonConfigTest.java b/manabom/src/test/java/mannabom_server/manabom/infrastructure/config/JacksonConfigTest.java index cb26c62..1d5796a 100644 --- a/manabom/src/test/java/mannabom_server/manabom/infrastructure/config/JacksonConfigTest.java +++ b/manabom/src/test/java/mannabom_server/manabom/infrastructure/config/JacksonConfigTest.java @@ -27,8 +27,13 @@ void serializesExternalLongIdsAsStrings() throws Exception { JsonNode json = objectMapper.readTree(objectMapper.writeValueAsString(payload)); + assertThat(json.get("userId").isTextual()).isTrue(); assertThat(json.get("userId").asText()).isEqualTo("10"); + + assertThat(json.get("recipientUserIds").get(0).isTextual()).isTrue(); assertThat(json.get("recipientUserIds").get(0).asText()).isEqualTo("20"); + + assertThat(json.get("data").get("roomId").isTextual()).isTrue(); assertThat(json.get("data").get("roomId").asText()).isEqualTo("30"); assertThat(json.get("totalCount").isIntegralNumber()).isTrue(); } @@ -46,7 +51,10 @@ void includesProfileIdInMatchedChatParticipant() throws Exception { JsonNode json = objectMapper.readTree(objectMapper.writeValueAsString(participant)); + assertThat(json.get("userId").isTextual()).isTrue(); assertThat(json.get("userId").asText()).isEqualTo("10"); + + assertThat(json.get("profileId").isTextual()).isTrue(); assertThat(json.get("profileId").asText()).isEqualTo("100"); } From 71459f37b5ad779dcdc5a35786d7a1a95631de24 Mon Sep 17 00:00:00 2001 From: sehi55 Date: Thu, 6 Aug 2026 18:47:47 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=EC=8B=A0=EA=B3=A0=20=EC=9D=B4?= =?UTF-8?q?=EC=9C=A0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/report/controller/ReportControllerTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manabom/src/test/java/mannabom_server/manabom/presentation/report/controller/ReportControllerTest.java b/manabom/src/test/java/mannabom_server/manabom/presentation/report/controller/ReportControllerTest.java index 075c63b..ce182bc 100644 --- a/manabom/src/test/java/mannabom_server/manabom/presentation/report/controller/ReportControllerTest.java +++ b/manabom/src/test/java/mannabom_server/manabom/presentation/report/controller/ReportControllerTest.java @@ -4,6 +4,7 @@ import mannabom_server.manabom.application.report.dto.request.CreateProfileReportRequest; import mannabom_server.manabom.application.report.dto.response.CreateReportResponse; import mannabom_server.manabom.application.report.service.ReportService; +import mannabom_server.manabom.domain.report.entity.ReportReason; import org.junit.jupiter.api.Test; import org.springframework.http.ResponseEntity; @@ -20,6 +21,7 @@ class ReportControllerTest { void returnsCreatedReportIdForProfileReport() { CreateProfileReportRequest request = CreateProfileReportRequest.builder() .profileId(200L) + .reason(ReportReason.INAPPROPRIATE_PROFILE) .build(); when(reportService.createProfileReport(1L, request)).thenReturn(300L);