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

[FEAT] 신고 시 욕설 확인 기능 구현 #228

Merged
merged 11 commits into from
Sep 22, 2024
Merged
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 @@ -31,6 +31,10 @@ public boolean isBlocked() {
return isBlocked;
}

public void block() {
this.isBlocked = true;
}

public CheerTalk(final String content, final Long gameTeamId) {
this.createdAt = LocalDateTime.now();
this.content = content;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.sports.server.command.report.application;

import com.sports.server.command.cheertalk.domain.CheerTalk;
import com.sports.server.command.report.domain.Report;
import com.sports.server.common.application.TextFileProcessor;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class ReportProcessor {

private final TextFileProcessor textFileProcessor;
private static final String FILE_NAME = "static/extra_bad_words.txt";
private static final String DELIM = ",";

private Set<String> cachedBadWords;

public void check(CheerTalk cheerTalk, Report report) {

if (cachedBadWords == null) {
cachedBadWords = textFileProcessor.readFile(FILE_NAME, DELIM);
}

if (cachedBadWords.contains(cheerTalk.getContent())) {
report.updateToValid();
return;
}
report.updateToPending();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,13 @@ private void validateBlockedCheerTalk(CheerTalk cheerTalk) {
public boolean isUnchecked() {
return this.state == ReportState.UNCHECKED;
}

public void updateToValid() {
this.state = ReportState.VALID;
cheerTalk.block();
}

public void updateToPending() {
this.state = ReportState.PENDING;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package com.sports.server.command.report.infrastructure;

import com.sports.server.command.cheertalk.domain.CheerTalk;
import com.sports.server.command.report.application.ReportProcessor;
import com.sports.server.command.report.domain.Report;
import com.sports.server.command.report.domain.ReportEvent;
import com.sports.server.command.report.exception.ReportErrorMessage;
import com.sports.server.common.exception.CustomException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
Expand All @@ -16,7 +13,7 @@
@RequiredArgsConstructor
public class ReportEventHandler {

private final ReportCheckClient reportCheckClient;
private final ReportProcessor reportProcessor;
Copy link
Contributor

@ldk980130 ldk980130 Sep 21, 2024

Choose a reason for hiding this comment

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

어차피 나중에 다시 람다 서버를 쓰게 되면서 기존 FeignClient를 쓰는 ReportCheckClient로 변경할 예정인거지?

그거와 별개로 ReportEventHandler의 내부 코드를 전혀 건드리지 않으면서 이번 PR을 마무리하는 방법을 썼으면 더 좋았을 것 같은데 이건 내가 ReportCheckClient 인터페이스 설계를 잘못해서 애초에 무리였군 (반환값이 ResponseEntity라..)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

맞아!
ReportCheckClient 인터페이스 설계와는 별개로 ReportEventHandler 의 내부 코드를 건드리지 않고 신고 기능을 구현할 수 있었던 걸까?! 나는 건드리지 않는 방법이 떠오르지 않는데 어떤 방식을 생각했던 건지 물어봐도 될까?

Copy link
Contributor

@ldk980130 ldk980130 Sep 21, 2024

Choose a reason for hiding this comment

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

ReportCheckClient 인터페이스가 지금은 HTTP 요청을 한다는 거를 ResponseEntity를 반환하고 있어서 지금은 코드 변경이 필연적일 수밖에 없어.

그런데 만약 인터페이스를 HTTP 의존적이지 않게 짰다면 ReportCheckClientReportProcesor가 구현하게 하면 핸들러는 ReportCheckClient를 의존한 코드 그대로 사용할 수 있었겠지.

그래서 이번참에 신고 처리 인터페이스를 제네럴하게 구현해서 ReportEventHandler가 걔를 의존하게 하고, 그 인터페이스 구현체를 '람다 서버 이용하는 구현체', '내부 파일 이용하는 구현체'로 다르게 구현할 수 있을 것 같은데 리팩터링해보면 어떨까?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

아하 그렇겠다 무슨 말인지 이해했어!
이거까지 적용해서 수정해볼게

Copy link
Contributor Author

Choose a reason for hiding this comment

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

추후 리팩토링 하기!


@TransactionalEventListener
@Async("asyncThreadPool")
Expand All @@ -29,16 +26,32 @@ public void handle(ReportEvent event) {

private void checkReport(Report report) {
CheerTalk cheerTalk = report.getCheerTalk();
ReportCheckRequest request = new ReportCheckRequest(
cheerTalk.getContent(), cheerTalk.getId(), report.getId()
);
ResponseEntity<Void> response = reportCheckClient.check(request);
validateResponse(response);
reportProcessor.check(cheerTalk, report);
}

private void validateResponse(ResponseEntity<Void> response) {
if (response.getStatusCode().is5xxServerError()) {
throw new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, ReportErrorMessage.REPORT_CHECK_SERVER_ERROR);
}
}
// 추후 람다로 이전 시 필요한 메서드들

// @TransactionalEventListener
// @Async("asyncThreadPool")
// public void handle(ReportEvent event) {
// Report report = event.report();
// if (report.isUnchecked()) {
// checkReport(report);
// }
// }
//
// private void checkReport(Report report) {
// CheerTalk cheerTalk = report.getCheerTalk();
// ReportCheckRequest request = new ReportCheckRequest(
// cheerTalk.getContent(), cheerTalk.getId(), report.getId()
// );
// ResponseEntity<Void> response = reportCheckClient.check(request);
// validateResponse(response);
// }
//
// private void validateResponse(ResponseEntity<Void> response) {
// if (response.getStatusCode().is5xxServerError()) {
// throw new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, ReportErrorMessage.REPORT_CHECK_SERVER_ERROR);
// }
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.sports.server.common.application;

import com.sports.server.common.exception.CustomException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

@Service
public class TextFileProcessor {

public Set<String> readFile(String fileName, String delim) {
ClassPathResource resource = new ClassPathResource(fileName);

Set<String> resultSet = new HashSet<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {

String line;
while ((line = reader.readLine()) != null) {
String[] tokens = line.split(delim);
for (String token : tokens) {
resultSet.add(token.replaceAll("^[\\s'\"]+|[\\s'\"]+$", ""));
}
}
} catch (IOException e) {
throw new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "파일을 읽어들이는 도중 예외가 발생했습니다.");
}
return resultSet;
}

}

1 change: 1 addition & 0 deletions src/main/resources/static/extra_bad_words.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
'^^ 7', '^ ^ 7', '^^7', '^7', ';;', '^노^', ' ̄', '⊥', '─ ─', '━ ━', '──', '━━', '┴', '้', '̆̈', '1빠', '==', '2빠', '2찍', 'D쥐고', 'D지고', 'jonna', 'jot같', 'mi쳤', 'tlqkf', '&stype=9', 'wlfkf', '갈틱폰', 'gartic', '같은 새끼', '같은새끼', '개 새끼', '개같아', '개같은', '개같을', '개같게', '개나 소나', '개나대', '개나소나', '개넷', '개년', '개념빠가', '느갭', '느검', '도태남', '도태녀', '개독', '개돼지', '개련', '개련', '개부랄', '개삼성', '개새기', '개새끼', '개색', '개섹', '풀발', '씹선', '개셈', '개소리', '개쓰래기', '개저씨', '개줌마', '계새끼', '골 빈', '골1빈', '골빈', '괘새끼', '그1켬', '김치녀', '김치남', '김치놈', '김치년', '한녀', '한.녀', '한남들', '한.남', '그남들', '자들자들', '된장녀', '피싸개', '앙기모띠', '소추', '퍄퍄', '형보수지', '눈나', '김여사', '여적여', '자적자', '남적남', '보적보', '삼일한', '보슬아치', '보징어', '엑윽', '헤으응', '이기야', '부왘', '보픈카', '상폐녀', '배빵', '누보햄', '자박꼼', '로린 ', '아몰랑', '업계포상', '번녀', '번남', '대남', '대녀', '냄저', '빨갱', '뷔페미', '꼴페', '-2-', '-1-', '문재앙', '윤재앙', '펨베', '펨코', '펨.코', '엠팍', '쿰.척', '쿰척', 'ㅗㅜㅑ', '오우야', '껒여', '꺼지세요', '꺼져요', '로 꺼져', '로꺼져', '로 꺼.져', '꺼.지', '꼴데', '설거지론', '퐁퐁남', '퐁퐁녀', '나쁜 새끼', '년놈', '노알라', '느그', '느금', '뇌 텅', '뇌1텅', '뇌텅', '눈깔 파', '눈깔파', '늬믜', '늬미', '니년', '니믜', '니미럴', '닝기리', '닥쳐라', '닥치세', '대가리', '머가리', '머.가리', '대.가리', '덬', '도라이', '뒈져', '뒤져라', '뒤져버', '뒤져야', '뒤져야지', '뒤져요', '뒤졌', '뒤지겠', '뒤지고싶', '뒤지길', '뒤진다', '뒤질', '디져라', '디졌', '디지고', '디질', '딴년', '아재요', '아재는', '네 아줌마', '네 아저씨', '네아줌마', '네아저씨', '뚝배기깨', '뚝배기 깨', '뚫린 입', '뚫린입', '라면갤', '런놈', '런년', '럼들', '레1친', '레기같', '레기네', '레기다', '레친', 'xy들', 'xx들', '련들', '롬들', 'ㅁ.ㄱ', 'ㅁㅊ', 'ㅁ친', '맘충', '🤏🏻', '🤏', '✖️', '망돌', '머갈', '먹 금', '먹.금', '먹.끔', '먹1금', '먹금', '먹끔', '명존', '뭔솔', '미놈', '미시친발', '미쳣네', '미쳤니', '미친 새', '미친~', '미친개', '미친새', '미친색', '줘패', '꼬추', '미치ㄴ', 'ㅅ.ㄲ', '색퀴', 'ㅅ끼', '한남들', '흉자', '&feature=youtu.be', 'GR도', '미핀놈', '샛기', '폐급', 'xportsnews', 'G랄', '세키', 'd져', 'ㅂㅁㄱ', 'ㅂㅊ', 'ㅂ크', '발놈', '별창', '병1신', '병신', '봊', '보전깨', '싸개', '븅신', '빠큐', '빡새끼', '뻐규', '뻐큐', '뻑유', '뻑큐', '뻨큐', '뼈큐', '쉰내', 'ㅄ', 'ㅅ', 'ㅂ', 'ㅅ.ㅂ', 'ㅅ1ㅂ', 'ㅅ1발', 'ㅅㄲ네', 'ㅅㅋ네', 'ㅅㄲ들', 'ㅅㅋ들', '친ㅅㄲ', '친 ㅅㄲ', 'ㅅ1ㄲ', 'ㅅㅌㅊ', '사새끼', '새.끼', '새1끼', '새1키', '새77ㅣ', '새끼라', '새끼야', '새퀴', '새킈', '새키', '색희', '색히', '샊기', '샊히', '샹년', '섀키', '서치해', '섬숭이', '성괴', '솔1친', '솔친', '쉬발', '쉬버', '쉬이바', '쉬이이', '쉬펄', '슈1발', '슈레기', '슈발', '슈벌', '슈우벌', '슈ㅣ발', '스벌', '슨상님', '싑창', '시1발', '시미발친', '시미친발', '시바류', '시바알', '시발', 'toss.im', 'metavv', 'newspic', 'salgoonews', 'ㅅㅂ', 'ㅅ.ㅂ', '닥 쳐', '하남자', '하 남자', '하여자', '하 여자', '쌉스', '썩열', '썩렬', '쎡열', '쎡렬', '먹버', '대깨', '야랄', 'ㅂㅅ', 'ㅂ.ㅅ', 'ㅂ1ㅅ', 'ㅅ1ㅂ', '한남노', '한남들', '한.남', '한1남', '한남을', '싸튀', '멍.청', '- 2 -', '- 1 -', '아줌내', '머깨', '등신아', '미친것', '개때리', '개떄려', '염병하', '염병짓', '종간나', '빠가사리', '새기들', '애새기', 'ktestone', '🖕', '시방새', '시벌탱', '시볼탱', '시부럴', '시부렬', '시부울', '시뷰럴', '시뷰렬', '시빨', '시새발끼', '시이발', '시친발미', '시팔', '시펄', '십창', '퐁퐁단', '십팔', 'ㅆ1ㄺ', 'ㅆ1ㅂ', 'ㅆㄹㄱ', 'ㅆㄺ', 'ㅆㅂ', '싸물어', '쌍년', '쌍놈', '쌔끼', '썅', '썌끼', '쒸펄', '쓰1레기', '쓰래기같', '쓰레기 새', '쓰레기새', '쓰렉', '씝창', '씨1발', '씨바라', '씨바알', '씨발', '씨.발', '씨방새', '씨버럼', '씨벌', '씨벌탱', '씨볼탱', '씨부럴', 'link.coupang', 'jigex.com', '씨부렬', '씨뷰럴', '씨뷰렬', '씨빠빠', '씨빨', '씨뻘', '씨새발끼', '씨이발', '씨팔', '씹귀', '씹못', 'kko.to', '씹뻐럴', '씹새끼', '씹쌔', '씹창', '씹치', '씹팔', '씹할', '아가리', '❌', '아닥', '더쿠', '덬', '더.쿠', 'ㄷㅋ', '아오 ㅅㅂ', '아오 시바', '아오ㅅㅂ', '아오시바', '안물안궁', '애미', '앰창', '닥눈삼', '에라이 퉤', '에라이 퉷', '에라이퉤', '에라이퉷', '엠뷩신', '엠븽신', '엠빙신', '엠생', '엠창', '엿같', '엿이나', '옘병', '외1퀴', '외퀴', '웅앵', '웅엥', '은년', '은새끼', '이 새끼', '이새끼', '一 一', '一 ㅡ', '一一', '一ㅡ', '입 털', '입털', 'ㅈ.ㄴ', 'ㅈ소', 'ㅈㄴ', 'ㅈㄹ', '정신나갓', '정신나갔', '젖 같', '젗같', '젼나', '젼낰', '졀라', '졀리', '졌같은', '졏 같', '조온', '조온나', '족까', '존 나', '존 나', '존', '나', '존.나', '존1', '존1나', '🚬', '멍청', '?feature=share', '능지', '조센징', '짱깨', '짱개', '짱꼴라', '착짱', '죽짱', '착.짱', '죽.짱', '착1짱', '죽1짱', '짱골라', '좃', '종나', '곱창났', '곱창나', '좆', '좁밥', '좋소', '좇같', '죠낸', '죠온나', '죤나', '죤내', '죵나', '죶', '죽어버려', '죽여 버리고', '죽여버리고', '죽여불고', '죽여뿌고', '줬같은', '쥐랄', '쥰나', '쥰내', '쥰니', '쥰트', '즤랄', '지 랄', '지1랄', '지1뢰', '지랄', 'ezr', '2zr', '2gr', '지롤', '석 렬', '썩 렬', '썩 열', '찢재', '찢 재', '찢1', 'ㅁ청', 'ㅉ', 'ㅉ질한', '짱깨', '짱께', '쪼녜', '착짱죽짱', '섬숭이', '쪽본', '쪽1바리', '쪽바리', '쪽발', '쫀 맛', '쫀1', '쫀귀', '쫀맛', '쫂', '쫓같', '쬰잘', '쬲', '쯰질', '찌1질', '찌질한', '찍찍이', '찎찎이', '찝째끼', '창년', '창녀', '창남', '창놈', '창넘', '처먹', '凸', '첫빠', '쳐마', '쳐먹', '쳐받는', '쳐발라', '취ㅈ', '취좃', '친 년', '한 년', '친 놈', '친구년', '친년', '한년', '친노마', '친놈', '친넘', 'ㅍㅌㅊ', '핑1프', '핑거프린세스', '핑끄', '핑프', 'ㅎㅃ', 'ㅎㅌㅊ', '손놈', '호로새끼', '호로잡', '화낭년', '화냥년', '후1빨', '후빨', 'ㅗ', 'ㅡ 一', 'ㅡ ㅡ', 'ㅡ一', '————', 'ㅡㅡ', 'ㅡㅡ', 'ㅡㅡ', 'ส', 'ค็', '้', '็', 'zoomin.cur', 'eventHostId', 'open.', 'onchat', 'ajax_comment', 'stub-extra', '-stub', 'line.me', 'ask.fm', 'adinc.co.kr', 'vonvon', '&feature=share', 'superfeed', 'lumieyes', 'theqoo', 'dmitory', '_enliple', 'apt_review', 'showcat', '쇼캣', '.pls', 'stype=3', "$('", 'bitly', 'poomang.com', 'comment_memo', 'vote.php', 'onmouse', 'onkeyup', 'newsnack', 'onkeydown'
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package com.sports.server.command.report.acceptance;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.verify;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;

import com.sports.server.command.report.dto.ReportRequest;
import com.sports.server.support.AcceptanceTest;
import io.restassured.RestAssured;
Expand All @@ -12,13 +19,6 @@
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.verify;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;

@Sql(scripts = "/report-fixture.sql")
class ReportAcceptanceTest extends AcceptanceTest {

Expand All @@ -37,7 +37,7 @@ class ReportTest {
// then
assertAll(
() -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()),
() -> verify(reportCheckClient).check(any())
() -> verify(reportProcessor).check(any(), any())
);
}

Expand All @@ -53,7 +53,7 @@ class ReportTest {
// then
assertAll(
() -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()),
() -> verify(reportCheckClient, never()).check(any())
() -> verify(reportProcessor, never()).check(any(), any())
);
}

Expand All @@ -69,7 +69,7 @@ class ReportTest {
// then
assertAll(
() -> assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()),
() -> verify(reportCheckClient, never()).check(any())
() -> verify(reportProcessor, never()).check(any(), any())
);
}

Expand All @@ -86,7 +86,7 @@ class ReportTest {
// then
assertAll(
() -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()),
() -> verify(reportCheckClient, times(2)).check(any())
() -> verify(reportProcessor, times(2)).check(any(), any())
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.sports.server.command.report.application;

import static com.sports.server.support.fixture.FixtureMonkeyUtils.entityBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;

import com.sports.server.command.cheertalk.domain.CheerTalk;
import com.sports.server.command.report.domain.Report;
import com.sports.server.command.report.domain.ReportState;
import com.sports.server.support.ServiceTest;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;

public class ReportProcessorTest extends ServiceTest {

@Autowired
private ReportProcessor reportProcessor;

@ParameterizedTest
@ValueSource(strings = {"개같아", "뒤질", "ezr"})
void 욕설이_포함된_댓글인_경우_신고의_상태가_VALID가_되고_댓글이_블락된다(String badWord) {

// given
CheerTalk cheerTalk = entityBuilder(CheerTalk.class)
.set("is_blocked", false)
.set("content", badWord).sample();

Report report = entityBuilder(Report.class)
.set("cheerTalk", cheerTalk)
.set("state", ReportState.UNCHECKED)
.sample();

// when
reportProcessor.check(cheerTalk, report);

// then
assertAll(
() -> assertThat(report.getState()).isEqualTo(ReportState.VALID),
() -> assertThat(report.getCheerTalk().isBlocked()).isEqualTo(true)
);
}

@ParameterizedTest
@ValueSource(strings = {"욕이 아님", "욕아님"})
void 욕설이_포함되지_않은_경우_신고의_상태가_PENDING이_된다(String badWord) {

// given
CheerTalk cheerTalk = entityBuilder(CheerTalk.class)
.set("is_blocked", false)
.set("content", badWord).sample();

Report report = entityBuilder(Report.class)
.set("cheerTalk", cheerTalk)
.set("state", ReportState.UNCHECKED)
.sample();

// when
reportProcessor.check(cheerTalk, report);

// then
assertThat(report.getState()).isEqualTo(ReportState.PENDING);
}
}
Loading
Loading