Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public static MatchedChatRoomInfo of(Long roomId, List<Participant> participants

public record Participant(
Long userId,
Long profileId,
Gender gender,
String nickname,
String profileImg
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mannabom_server.manabom.domain.report.entity.ReportReason;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateChatReportRequest {

@NotNull(message = "채팅방 ID는 필수입니다.")
@Positive(message = "채팅방 ID는 0보다 커야 합니다.")
private Long contextId;

@NotNull(message = "신고 대상 사용자 ID는 필수입니다.")
@Positive(message = "신고 대상 사용자 ID는 0보다 커야 합니다.")
private Long targetId;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@NotNull(message = "신고 사유는 필수입니다.")
private ReportReason reason;

@Size(max = 1000, message = "신고 상세 내용은 최대 1000자까지 입력할 수 있습니다.")
private String additionalDetail;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mannabom_server.manabom.domain.report.entity.ReportReason;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateProfileReportRequest {

@NotNull(message = "신고 대상 프로필 ID는 필수입니다.")
@Positive(message = "신고 대상 프로필 ID는 0보다 커야 합니다.")
private Long profileId;

@NotNull(message = "신고 사유는 필수입니다.")
private ReportReason reason;

@Size(max = 1000, message = "신고 상세 내용은 최대 1000자까지 입력할 수 있습니다.")
private String additionalDetail;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<ReportStatus> 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("존재하지 않는 채팅방입니다.");
}
Expand All @@ -58,13 +116,4 @@ private void validateChatReport(Long reporterId, Long targetId, Long roomId) {
throw new IllegalArgumentException("신고 대상자가 해당 채팅방의 참여자가 아닙니다.");
}
}

//특정 유저 신고 내역 조회
//처리해야할 신고 리스트 조회
//처리된 신고 리스트 조회
//전체 신고 리스트 조회?
//신고 처리하기



}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public List<MatchedChatRoomInfo.Participant> 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,
(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Report, Long> {
Optional<Report> findFirstByReporter_UserIdAndTarget_UserIdAndTypeAndContextIdAndStatusInOrderByIdAsc(
Long reporterId,
Long targetId,
ReportType type,
Long contextId,
Collection<ReportStatus> statuses
);

@Query("""
select r
from Report r
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
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;


@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select u from User u where u.userId = :userId")
Optional<User> findByIdForUpdate(@Param("userId") Long userId);

/**
* 카카오 ID로 사용자 조회
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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
);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,15 +21,27 @@ public class ReportController {
private final ReportService reportService;

@PostMapping("/chat")
public ResponseEntity<ApiResponse<Void>> reportChat(@RequestBody CreateReportRequest request, @AuthenticationPrincipal Long userId){
reportService.createReport(userId, request, ReportType.CHAT);
return ResponseEntity.ok(ApiResponse.success(null, "채팅 신고를 완료했습니다."));
public ResponseEntity<ApiResponse<CreateReportResponse>> 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<ApiResponse<Void>> reportProfile(@RequestBody CreateReportRequest request, @AuthenticationPrincipal Long userId){
reportService.createReport(userId, request, ReportType.PROFILE);
return ResponseEntity.ok(ApiResponse.success(null, "프로필 신고를 완료했습니다."));
public ResponseEntity<ApiResponse<CreateReportResponse>> reportProfile(
@Valid @RequestBody CreateProfileReportRequest request,
@AuthenticationPrincipal Long userId
) {
Long reportId = reportService.createProfileReport(userId, request);
return ResponseEntity.ok(ApiResponse.success(
CreateReportResponse.of(reportId),
"프로필 신고를 완료했습니다."
));
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading