이번에는 신고기능을 추가하였다.
유저 신고, 게시판 신고, 댓글 신고
총 3가지를 추가하였고,
누적신고가 일정수치를 넘을 시 해당 엔티티에 신고상태를 활성화시켜 주었다.
자세한 건 아래 코드를 보며 자세히 알아보자.
유저신고, 게시판 신고, 댓글 신고
모두 매우 유사한 형태이므로, 유저 신고를 중점으로 설명하겠다.
자세한 건 맨 아래 깃허브를 통해 코드를 확인할 수 있다.
MemberReport
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class MemberReport extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_report_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reporter_id",nullable = false)
@OnDelete(action = OnDeleteAction.NO_ACTION)
private Member reporter;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reported_id",nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Member reportedMember;
@Column(nullable = false)
private String content;
public MemberReport(Member reporter, Member reportedMember, String content) {
this.reporter = reporter;
this.reportedMember = reportedMember;
this.content = content;
}
}
간단하게 다대일로 신고하는 사람과 신고당하는 사람을 선언해 주었다.
만약 신고자가 삭제되어도 신고 기록은 남게 두었고, 신고당한 사람이 삭제되면 신고 기록도 삭제된다.
ReportController
@Api(value = "Report Controller", tags = "Report")
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/reports")
public class ReportController {
private final ReportService reportService;
private final MemberRepository memberRepository;
@ApiOperation(value = "게시글 신고", notes = "게시글을 신고합니다.")
@ResponseStatus(HttpStatus.OK)
@PostMapping("/boards")
public Response reportBoard(@Valid @RequestBody BoardReportRequestDto req) {
return Response.success(reportService.reportBoard(getPrincipal(), req));
}
@ApiOperation(value = "유저 신고", notes = "유저를 신고합니다.")
@ResponseStatus(HttpStatus.OK)
@PostMapping("/members")
public Response reportMember(@Valid @RequestBody MemberReportRequestDto req) {
return Response.success(reportService.reportMember(getPrincipal(), req));
}
@ApiOperation(value = "댓글 신고", notes = "댓글을 신고합니다.")
@ResponseStatus(HttpStatus.OK)
@PostMapping("/comments")
public Response reportComment(@Valid @RequestBody CommentReportRequestDto req) {
return Response.success(reportService.reportComment(getPrincipal(), req));
}
private Member getPrincipal() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Member member = memberRepository.findByUsername(authentication.getName()).orElseThrow(MemberNotFoundException::new);
return member;
}
}
각 DTO에는 신고할 id 값과 내용이 담겨있다.
유저신고, 댓글신고, 게시글 신고의 형태가 비슷한 것을 볼 수 있다.
ReportService
@Service
@RequiredArgsConstructor
public class ReportService {
private static final String SUCCESS_REPORT = "신고를 하였습니다.";
private final BoardReportRepository boardReportRepository;
private final MemberReportRepository memberReportRepository;
private final CommentReportRepository commentReportRepository;
private final MemberRepository memberRepository;
private final BoardRepository boardRepository;
private final CommentRepository commentRepository;
@Transactional
public String reportBoard(Member member, BoardReportRequestDto req){
Board reportedBoard = findReportedBoard(member, req.getReportedBoardId());
BoardReport boardReport =
new BoardReport(member, reportedBoard, req.getContent());
boardReportRepository.save(boardReport);
checkReportedBoard(reportedBoard);
return SUCCESS_REPORT;
}
private Board findReportedBoard(Member member, Long reportedBoardId){
Board board = boardRepository.findById(reportedBoardId).orElseThrow(BoardNotFoundException::new);
if (board.getMember().equals(member)){
throw new NotSelfReportException();
}
existReportedBoardHistory(member,board);
return board;
}
private void existReportedBoardHistory(Member member, Board board){
if (boardReportRepository.existsByReporterAndReportedBoard(member,board)){
throw new AlreadyReportException();
}
}
private void checkReportedBoard(Board reportedBoard){
if (boardReportRepository.findAllByReportedBoard(reportedBoard).size()>5){
reportedBoard.isReportedStatus();
}
}
@Transactional
public String reportMember(Member member, MemberReportRequestDto req){
Member reportedMember = findReportedMember(member, req.getReportedMemberId());
MemberReport memberReport =
new MemberReport(member, reportedMember, req.getContent());
memberReportRepository.save(memberReport);
checkReportedMember(reportedMember);
return SUCCESS_REPORT;
}
private Member findReportedMember(Member member, Long reportedMemberId){
Member reportedMember = memberRepository.findById(reportedMemberId).orElseThrow(MemberNotFoundException::new);
if (member.equals(reportedMember)){
throw new NotSelfReportException();
}
existReportedMemberHistory(member,reportedMember);
return reportedMember;
}
private void existReportedMemberHistory(Member member, Member reportedMember){
if (memberReportRepository.existsByReporterAndReportedMember(member,reportedMember)){
throw new AlreadyReportException();
}
}
private void checkReportedMember(Member reportedMember){
if (memberReportRepository.findAllByReportedMember(reportedMember).size()>5){
reportedMember.isReportedStatus();
}
}
@Transactional
public String reportComment(Member member, CommentReportRequestDto req){
Comment reportedComment = findReportedComment(member, req.getReportedCommentId());
CommentReport commentReport =
new CommentReport(member, reportedComment, req.getContent());
commentReportRepository.save(commentReport);
checkReportedComment(reportedComment);
return SUCCESS_REPORT;
}
private Comment findReportedComment(Member member, Long reportedCommentId){
Comment comment = commentRepository.findById(reportedCommentId).orElseThrow(CommentNotFoundException::new);
if (comment.getMember().equals(member)){
throw new NotSelfReportException();
}
existReportedCommentHistory(member,comment);
return comment;
}
private void existReportedCommentHistory(Member member, Comment comment){
if (commentReportRepository.existsByReporterAndReportedComment(member,comment)){
throw new AlreadyReportException();
}
}
private void checkReportedComment(Comment reportedComment){
if (commentReportRepository.findAllByReportedComment(reportedComment).size()>5){
reportedComment.isReportedStatus();
}
}
}
게시글 신고, 유저 신고, 댓글 신고 각각 3가지인데,
다 비슷한 로직이다.
신고할 대상이 존재하는지, 자기 자신을 신고하는지, 이미 신고하였는지를 판단한다.
이후 신고 객체를 만들고 저장하게 된다.
만약 신고한 대상의 누적 신고가 5번이 넘어가면 대상 필드에 있는 신고 상태가 true가 된다.
예를 들어 간단하게 유저의 경우 아래와 같이 신고에 대한 필드가 추가되었다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Column(nullable = false, unique = true)
private String username;
@JsonIgnore
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
@Enumerated(EnumType.STRING)
private Authority authority;
private boolean isReported;
@Builder
public Member(Long id, String username, String password, String name, Authority authority) {
this.id = id;
this.username = username;
this.password = password;
this.name = name;
this.authority = authority;
this.isReported = false;
}
public void modify(String password,String name) {
this.password = password;
this.name = name;
onPreUpdate();
}
public void isReportedStatus(){
isReported = true;
}
}
이렇게 신고가 활성화되어 있는 객체의 경우 나중에 어드민 페이지에서 찾아서 삭제를 하거나, 다른 조치를 취할 예정이다.
이제 테스트코드를 작성해 보자.
ReportControllerTest
@ExtendWith(MockitoExtension.class)
public class ReportControllerTest {
@InjectMocks
ReportController reportController;
@Mock
ReportService reportService;
@Mock
MemberRepository memberRepository;
MockMvc mockMvc;
ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
public void beforeEach() {
mockMvc = MockMvcBuilders.standaloneSetup(reportController).build();
}
@Test
public void 유저신고_테스트() throws Exception {
// given
MemberReportRequestDto req = new MemberReportRequestDto(1L, "내용");
Member member = createMember();
Authentication authentication = new UsernamePasswordAuthenticationToken(member.getId(), "", Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
given(memberRepository.findByUsername(authentication.getName())).willReturn(Optional.of(member));
// when, then
mockMvc.perform(
post("/api/reports/members")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isOk());
verify(reportService).reportMember(member, req);
}
@Test
public void 게시판신고_테스트() throws Exception {
// given
BoardReportRequestDto req = new BoardReportRequestDto(1L, "내용");
Member member = createMember();
Authentication authentication = new UsernamePasswordAuthenticationToken(member.getId(), "", Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
given(memberRepository.findByUsername(authentication.getName())).willReturn(Optional.of(member));
// when, then
mockMvc.perform(
post("/api/reports/boards")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isOk());
verify(reportService).reportBoard(member, req);
}
@Test
public void 댓글신고_테스트() throws Exception {
// given
CommentReportRequestDto req = new CommentReportRequestDto(1L, "내용");
Member member = createMember();
Authentication authentication = new UsernamePasswordAuthenticationToken(member.getId(), "", Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
given(memberRepository.findByUsername(authentication.getName())).willReturn(Optional.of(member));
// when, then
mockMvc.perform(
post("/api/reports/comments")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isOk());
verify(reportService).reportComment(member, req);
}
}
ReportServiceTest
@ExtendWith(MockitoExtension.class)
public class ReportServiceTest {
@InjectMocks
ReportService reportService;
@Mock
BoardReportRepository boardReportRepository;
@Mock
MemberReportRepository memberReportRepository;
@Mock
CommentReportRepository commentReportRepository;
@Mock
MemberRepository memberRepository;
@Mock
BoardRepository boardRepository;
@Mock
CommentRepository commentRepository;
@BeforeEach
@Test
public void 유저신고_테스트() {
// given
Member reporter = createMember();
Member reportedMember = Member.builder()
.id(3l)
.username("new")
.build();
MemberReportRequestDto req = new MemberReportRequestDto(reportedMember.getId(), "별로입니다.");
MemberReport memberReportHistory = new MemberReport(1L, reporter, reportedMember,req.getContent());
given(memberReportRepository.existsByReporterAndReportedMember(reporter,reportedMember)).willReturn(false);
given(memberRepository.findById(req.getReportedMemberId())).willReturn(Optional.of(reportedMember));
given(memberReportRepository.findAllByReportedMember(reportedMember)).willReturn(List.of(memberReportHistory));
// when
String result = reportService.reportMember(reporter, req);
// then
assertThat(result).isEqualTo("신고를 하였습니다.");
}
@Test
void 게시판신고_테스트() {
// given
Member reporter = createMember();
Member reportedMember = Member.builder()
.id(3l)
.username("new")
.build();
Board reportedBoard = createBoardWithMember(reportedMember);
BoardReportRequestDto req = new BoardReportRequestDto(reportedBoard.getId(), "별로입니다.");
BoardReport boardReport = new BoardReport(1L, reporter, reportedBoard, "content");
given(boardRepository.findById(req.getReportedBoardId())).willReturn(Optional.of(reportedBoard));
given(boardReportRepository.existsByReporterAndReportedBoard(reporter, reportedBoard)).willReturn(false);
given(boardReportRepository.findAllByReportedBoard(reportedBoard)).willReturn(List.of(boardReport));
// when
String result = reportService.reportBoard(reporter, req);
// then
assertThat(result).isEqualTo("신고를 하였습니다.");
}
@Test
void 댓글신고_테스트() {
// given
Member reporter = createMember();
Member reportedMember = Member.builder()
.id(3l)
.username("new")
.build();
Comment reportedComment = createComment(reportedMember);
CommentReportRequestDto req = new CommentReportRequestDto(reportedComment.getId(), "별로입니다.");
CommentReport commentReport = new CommentReport(1L, reporter, reportedComment, "content");
given(commentRepository.findById(req.getReportedCommentId())).willReturn(Optional.of(reportedComment));
given(commentReportRepository.existsByReporterAndReportedComment(reporter, reportedComment)).willReturn(false);
given(commentReportRepository.findAllByReportedComment(reportedComment)).willReturn(List.of(commentReport));
// when
String result = reportService.reportComment(reporter, req);
// then
assertThat(result).isEqualTo("신고를 하였습니다.");
}
}
테스트가 잘 성공한 것을 볼 수 있다.
이제 포스트맨을 사용하여 확인해 보자.
게시글, 유저, 댓글 신고의 경우 같은 로직이기도 하고 겹치는 부분이 많기 때문에 유저신고만 진행하였다.
유저를 1~7까지 만들어준 뒤 1번 유저로 로그인을 하고 진행하였다.
본인은 신고할 수가 없다.
6번 유저를 신고해 보자.
신고 1번의 경우 테스트용으로 이전에 한 것이다.
신고 2번인 1번 유저가 6번 유저에 대한 신고가 잘 작동되었다.
이 상태에서 1번 더 하게 되면 아래와 같은 예외가 발생한다.
이제 만약 누적신고를 받게 되면 어떻게 될까?
우선 현재 멤버의 값이다.
isReported값이 0인 이유는 false 상태이기 때문이다.
만약 신고를 당한 횟수가 5회가 초과되면 1로 바뀌게 된다.
이를 테스트해 보자.
각 유저 1~6번까지 돌아가며 7번 유저를 신고해 보자.
각 신고 내역이다.
현재 7번 유저가 6번의 신고를 당하였다.
따라서 7번 유저의 신고상태가 신고누적으로 바뀌었다.
이제 스웨거에 추가된 모습을 살펴보자.
스웨거에도 우리가 기대하는 대로 추가되었다.
자세한 코드는 아래 깃허브를 통해 볼 수 있다.
https://github.com/kimtaesoo99/community
'프로젝트 > 커뮤니티' 카테고리의 다른 글
[프로젝트] 커뮤니티 REST API 서버만들기 #9 카테고리 API 만들기 (0) | 2023.01.29 |
---|---|
[프로젝트] 커뮤니티 REST API 서버만들기 #8 어드민 페이지 만들기 (4) | 2023.01.28 |
[프로젝트] 커뮤니티 REST API 서버만들기 #6 Comment API만들기 (0) | 2023.01.25 |
[프로젝트] 커뮤니티 REST API 서버만들기 #5 게시판 부가기능 추가 (0) | 2023.01.24 |
[프로젝트] 커뮤니티 REST API 서버만들기 #4 Board API만들기 (0) | 2023.01.23 |