Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

신고 조치 기능 구현 #770

Merged
merged 11 commits into from
Oct 18, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public enum MemberExceptionType implements ExceptionType {
INVALID_NICKNAME_LENGTH(800, "닉네임의 길이가 올바르지 않습니다."),
INVALID_NICKNAME_LETTER(801, "닉네임에 들어갈 수 없는 문자가 포함되어 있습니다."),
ALREADY_EXISTENT_NICKNAME(802, "이미 중복된 닉네임이 존재합니다."),
NONEXISTENT_MEMBER(803, "해당 회원이 존재하지 않습니다."),
NON_EXISTENT_MEMBER(803, "해당 회원이 존재하지 않습니다."),
INVALID_AGE(804, "존재할 수 없는 연령입니다."),
ALREADY_ASSIGNED_GENDER(805, "이미 성별이 할당되어 있습니다."),
ALREADY_ASSIGNED_BIRTH_YEAR(806, "이미 출생년도가 할당되어 있습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public Member register(final Member member) {
@Transactional(readOnly = true)
public Member findById(final Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException(MemberExceptionType.NONEXISTENT_MEMBER));
.orElseThrow(() -> new NotFoundException(MemberExceptionType.NON_EXISTENT_MEMBER));
}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.votogether.domain.report.controller;

import com.votogether.domain.member.entity.Member;
import com.votogether.domain.report.dto.request.ReportActionRequest;
import com.votogether.domain.report.dto.request.ReportRequest;
import com.votogether.domain.report.service.ReportCommandService;
import com.votogether.global.jwt.Auth;
Expand All @@ -23,4 +24,10 @@ public ResponseEntity<Void> report(@Valid @RequestBody final ReportRequest reque
return ResponseEntity.ok().build();
}

@PostMapping("/reports/action/admin")
public ResponseEntity<Void> reportAction(@Valid @RequestBody final ReportActionRequest request) {
reportCommandService.reportAction(request);
return ResponseEntity.ok().build();
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.votogether.domain.report.controller;

import com.votogether.domain.member.entity.Member;
import com.votogether.domain.report.dto.request.ReportActionRequest;
import com.votogether.domain.report.dto.request.ReportRequest;
import com.votogether.global.exception.ExceptionResponse;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -9,6 +10,7 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Positive;
import org.springframework.http.ResponseEntity;

@Tag(name = "신고", description = "신고 API")
Expand All @@ -30,4 +32,25 @@ public interface ReportCommandControllerDocs {
})
ResponseEntity<Void> report(final ReportRequest request, final Member member);

@Operation(summary = "신고 조치", description = "신고를 조치한다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "신고 조치 성공"
),
@ApiResponse(
responseCode = "400",
description = """
1.신고 ID가 양의 정수가 아닌 경우
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

신고 ID가 존재하지 않은 경우에도 400 예외가 발생할 수 있을 것 같아요 !

""",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 신고",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
)
})
ResponseEntity<Void> reportAction(final ReportActionRequest request);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.votogether.domain.report.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

@Schema(description = "신고 조치 요청")
public record ReportActionRequest(
@Schema(description = "신고 ID", example = "1")
@NotNull(message = "신고 ID는 빈 값일 수 없습니다.")
@Positive(message = "신고 ID는 양의 정수만 가능합니다.")
Long id,

@Schema(description = "신고 조치 여부", example = "true")
boolean hasAction
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public enum ReportExceptionType implements ExceptionType {
DUPLICATE_COMMENT_REPORT(1205, "하나의 댓글에 대해서 중복하여 신고할 수 없습니다."),
REPORT_MY_NICKNAME(1206, "자신의 닉네임은 신고할 수 없습니다."),
DUPLICATE_NICKNAME_REPORT(1207, "하나의 닉네임에 대해서 중복하여 신고할 수 없습니다."),
NOT_FOUND(1208, "신고가 존재하지 않습니다.")
;

private final int code;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.votogether.domain.report.repository;

import com.votogether.domain.report.dto.ReportAggregateDto;
import com.votogether.domain.report.entity.vo.ReportType;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Pageable;

public interface ReportCustomRepository {

List<ReportAggregateDto> findReportsGroupedByReportTypeAndTargetId(final Pageable pageable);
List<ReportAggregateDto> findReportAggregateDtosByReportTypeAndTargetId(final Pageable pageable);

Optional<ReportAggregateDto> findReportAggregateDtoByReportTypeAndTargetId(ReportType reportType, Long targetId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final 키워드가 빠져있어요 !


}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.votogether.domain.report.dto.ReportAggregateDto;
import com.votogether.domain.report.entity.vo.ReportType;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
Expand All @@ -18,7 +20,7 @@ public class ReportCustomRepositoryImpl implements ReportCustomRepository {
private final JPAQueryFactory jpaQueryFactory;

@Override
public List<ReportAggregateDto> findReportsGroupedByReportTypeAndTargetId(final Pageable pageable) {
public List<ReportAggregateDto> findReportAggregateDtosByReportTypeAndTargetId(final Pageable pageable) {
return jpaQueryFactory.select(
Projections.constructor(
ReportAggregateDto.class,
Expand All @@ -37,4 +39,31 @@ public List<ReportAggregateDto> findReportsGroupedByReportTypeAndTargetId(final
.fetch();
}

@Override
public Optional<ReportAggregateDto> findReportAggregateDtoByReportTypeAndTargetId(
final ReportType reportType,
final Long targetId
) {
final ReportAggregateDto result = jpaQueryFactory.select(
Projections.constructor(
ReportAggregateDto.class,
report.id.max(),
report.reportType,
report.targetId,
Expressions.stringTemplate("group_concat({0})", report.reason),
report.createdAt.max()
)
)
.from(report)
.where(
report.reportType.eq(reportType),
report.targetId.eq(targetId)
)
.groupBy(report.reportType, report.targetId)
.fetchOne();

return Optional.ofNullable(result);
}


}
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.votogether.domain.report.service;

import com.votogether.domain.member.entity.Member;
import com.votogether.domain.report.dto.ReportAggregateDto;
import com.votogether.domain.report.dto.request.ReportActionRequest;
import com.votogether.domain.report.dto.request.ReportRequest;
import com.votogether.domain.report.entity.Report;
import com.votogether.domain.report.exception.ReportExceptionType;
import com.votogether.domain.report.repository.ReportRepository;
import com.votogether.domain.report.service.strategy.ReportActionProvider;
import com.votogether.domain.report.service.strategy.ReportStrategy;
import com.votogether.global.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -14,10 +20,29 @@
public class ReportCommandService {

private final ReportActionProvider reportActionProvider;
private final ReportRepository reportRepository;

public void report(final Member reporter, final ReportRequest request) {
final ReportStrategy reportStrategy = reportActionProvider.getStrategy(request.type());
reportStrategy.report(reporter, request);
}

public void reportAction(final ReportActionRequest request) {
final Report report = reportRepository.findById(request.id())
.orElseThrow(() -> new NotFoundException(ReportExceptionType.NOT_FOUND));

final ReportAggregateDto reportAggregateDto = reportRepository
.findReportAggregateDtoByReportTypeAndTargetId(report.getReportType(), report.getTargetId())
.orElseThrow(() -> new NotFoundException(ReportExceptionType.NOT_FOUND));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q
이 두 상황에서 같은 예외를 사용하는 이유가 무엇인가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

먼저 저 두 단계를 거칠 필요가 있었습니다. 결국 reportType과 targetId로 group by 해서 가져와야 해서요.

그래서 이처럼 2번 가져오게 되었는데, 결국 id로든 reportType과 targetId 로든 결국 찾아오려고 하는데 못찾아오는 거면 NotFoundException이 맞다고 판단했습니다. 또한 둘 다 Report에 관련한 것들이니 ReportExceptionType을 사용하였습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외 메시지가 같은데, 예외가 발생한 경우 응답과 로그를 통해서 찾기 어렵지 않을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러게요. ReportAggregateDto에 관한 message를 따로 만들어볼게요.


reportRepository.deleteAllWithReportTypeAndTargetIdInBatch(report.getReportType(), report.getTargetId());

if (!request.hasAction()) {
return;
}

final ReportStrategy strategy = reportActionProvider.getStrategy(reportAggregateDto.reportType());
strategy.reportAction(reportAggregateDto);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public ReportPageResponse getReports(final int page) {

final Pageable pageable = PageRequest.of(page, BASIC_PAGE_SIZE);
final List<ReportAggregateDto> reportAggregateDtos = reportRepository
.findReportsGroupedByReportTypeAndTargetId(pageable);
.findReportAggregateDtosByReportTypeAndTargetId(pageable);
final List<ReportResponse> reportResponses = parseReportResponses(reportAggregateDtos);

return ReportPageResponse.of(totalPageNumber, page, reportResponses);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.votogether.domain.report.service.strategy;

import com.votogether.domain.alarm.entity.ReportActionAlarm;
import com.votogether.domain.alarm.repository.ReportActionAlarmRepository;
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.post.entity.comment.Comment;
import com.votogether.domain.post.exception.CommentExceptionType;
import com.votogether.domain.post.repository.CommentRepository;
import com.votogether.domain.report.dto.ReportAggregateDto;
import com.votogether.domain.report.dto.request.ReportRequest;
import com.votogether.domain.report.exception.ReportExceptionType;
import com.votogether.domain.report.repository.ReportRepository;
Expand All @@ -16,10 +19,9 @@
@Component
public class ReportCommentStrategy implements ReportStrategy {

private static final int NUMBER_OF_COMMENT_BLIND_BASED_REPORTS = 5;

private final CommentRepository commentRepository;
private final ReportRepository reportRepository;
private final ReportActionAlarmRepository reportActionAlarmRepository;

@Override
public void report(final Member reporter, final ReportRequest request) {
Expand All @@ -28,7 +30,6 @@ public void report(final Member reporter, final ReportRequest request) {
validateComment(reporter, request, reportedComment);

saveReport(reporter, request, reportRepository);
blindComment(request, reportedComment);
}

private void validateComment(
Expand All @@ -46,13 +47,6 @@ private void validateComment(
);
}

private void blindComment(final ReportRequest request, final Comment reportedComment) {
final int reportCount = reportRepository.countByReportTypeAndTargetId(request.type(), request.id());
if (reportCount >= NUMBER_OF_COMMENT_BLIND_BASED_REPORTS) {
reportedComment.blind();
}
}

private void validateHiddenComment(final Comment comment) {
if (comment.isHidden()) {
throw new BadRequestException(CommentExceptionType.IS_HIDDEN);
Expand All @@ -72,4 +66,21 @@ public String parseTarget(final Long targetId) {
return reportedComment.getContent();
}

@Override
public void reportAction(final ReportAggregateDto reportAggregateDto) {
final Comment comment = commentRepository.findById(reportAggregateDto.targetId())
.orElseThrow(() -> new NotFoundException(CommentExceptionType.NOT_FOUND));

final ReportActionAlarm reportActionAlarm = ReportActionAlarm.builder()
.member(comment.getWriter())
.reportType(reportAggregateDto.reportType())
.target(comment.getContent())
.reasons(reportAggregateDto.reasons())
.isChecked(false)
.build();

reportActionAlarmRepository.save(reportActionAlarm);
comment.blind();
}

}
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
package com.votogether.domain.report.service.strategy;

import com.votogether.domain.alarm.entity.ReportActionAlarm;
import com.votogether.domain.alarm.repository.ReportActionAlarmRepository;
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.member.exception.MemberExceptionType;
import com.votogether.domain.member.repository.MemberRepository;
import com.votogether.domain.report.dto.ReportAggregateDto;
import com.votogether.domain.report.dto.request.ReportRequest;
import com.votogether.domain.report.entity.vo.ReportType;
import com.votogether.domain.report.exception.ReportExceptionType;
import com.votogether.domain.report.repository.ReportRepository;
import com.votogether.global.exception.BadRequestException;
import com.votogether.global.exception.NotFoundException;
import java.util.Objects;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class ReportNicknameStrategy implements ReportStrategy {

private static final int NUMBER_OF_NICKNAME_CHANGE_REPORTS = 3;

private final MemberRepository memberRepository;
private final ReportRepository reportRepository;
private final ReportActionAlarmRepository reportActionAlarmRepository;

@Override
public void report(final Member reporter, final ReportRequest request) {
final Member reportedMember = memberRepository.findById(request.id())
.orElseThrow(() -> new NotFoundException(MemberExceptionType.NONEXISTENT_MEMBER));
validateMemberExistence(request);
validateNickname(reporter, request);

saveReport(reporter, request, reportRepository);
changeNicknameByReport(reportedMember, request);
}

private void validateMemberExistence(final ReportRequest request) {
final Optional<Member> memberById = memberRepository.findById(request.id());
if (memberById.isEmpty()) {
throw new NotFoundException(MemberExceptionType.NON_EXISTENT_MEMBER);
}
}

private void validateNickname(final Member reporter, final ReportRequest request) {
Expand All @@ -48,19 +54,28 @@ private void validateMyNickname(final Member reporter, final ReportRequest reque
}
}

private void changeNicknameByReport(final Member reportedMember, final ReportRequest request) {
final int reportCount = reportRepository.countByReportTypeAndTargetId(request.type(), reportedMember.getId());
if (reportCount >= NUMBER_OF_NICKNAME_CHANGE_REPORTS) {
reportedMember.changeNicknameByReport();
reportRepository.deleteAllWithReportTypeAndTargetIdInBatch(ReportType.NICKNAME, request.id());
}
}

@Override
public String parseTarget(final Long targetId) {
final Member reportedMember = memberRepository.findById(targetId)
.orElseThrow(() -> new NotFoundException(MemberExceptionType.NONEXISTENT_MEMBER));
.orElseThrow(() -> new NotFoundException(MemberExceptionType.NON_EXISTENT_MEMBER));
return reportedMember.getNickname();
}

@Override
public void reportAction(final ReportAggregateDto reportAggregateDto) {
final Member reportedMember = memberRepository.findById(reportAggregateDto.targetId())
.orElseThrow(() -> new NotFoundException(MemberExceptionType.NON_EXISTENT_MEMBER));

final ReportActionAlarm reportActionAlarm = ReportActionAlarm.builder()
.member(reportedMember)
.reportType(reportAggregateDto.reportType())
.target(reportedMember.getNickname())
.reasons(reportAggregateDto.reasons())
.isChecked(false)
.build();

reportActionAlarmRepository.save(reportActionAlarm);
reportedMember.changeNicknameByReport();
}

}
Loading