Skip to content

Commit 3cf5ecc

Browse files
committed
merge
2 parents 830ce61 + bfe3c5e commit 3cf5ecc

File tree

16 files changed

+211
-103
lines changed

16 files changed

+211
-103
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ dependencies {
7979

8080
// Swagger
8181
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.0'
82+
83+
implementation 'org.ahocorasick:ahocorasick:0.6.3'
8284
}
8385

8486
tasks.named('test') {

src/main/java/io/crops/warmletter/domain/badword/service/BadWordService.java

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
import io.crops.warmletter.domain.badword.repository.BadWordRepository;
1313
import jakarta.transaction.Transactional;
1414
import lombok.RequiredArgsConstructor;
15+
import org.ahocorasick.trie.Emit;
16+
import org.ahocorasick.trie.PayloadEmit;
17+
import org.ahocorasick.trie.Trie;
1518
import org.springframework.data.redis.core.RedisTemplate;
1619
import org.springframework.stereotype.Service;
1720

@@ -26,7 +29,9 @@ public class BadWordService {
2629
private final RedisTemplate<String, String> redisTemplate; // Redis 추가
2730

2831
private static final String BAD_WORD_KEY = "bad_word";
29-
private static final String BAD_WORD_PATTERN = "[^가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z0-9]";
32+
33+
private static final String BAD_WORD_PATTERN = "[^가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z0-9\\s]";
34+
3035

3136
public void createBadWord(CreateBadWordRequest request) {
3237
String word = request.getWord();
@@ -107,22 +112,31 @@ public void deleteBadWord(Long id) {
107112

108113
//필터링
109114
public void validateText(String text) {
115+
// Redis에서 금칙어 데이터를 불러옴
110116
Map<Object, Object> entries = redisTemplate.opsForHash().entries(BAD_WORD_KEY);
111117

118+
// 금칙어 목록을 Set으로 변환
112119
Set<String> badWords = entries.values().stream()
113120
.map(Object::toString)
114121
.collect(Collectors.toSet());
115122

116-
String sanitizedText = text.replaceAll(BAD_WORD_PATTERN, "");
117-
118-
123+
// 아호코라식 트리(Trie) 생성 (단어 단위 매칭, 대소문자 구분 없이)
124+
Trie.TrieBuilder builder = Trie.builder().onlyWholeWords().caseInsensitive();
119125
for (String badWord : badWords) {
120-
// 금칙어도 혹시 특수문자 있을 수 있으니까 정제
121-
String sanitizedBadWord = badWord.replaceAll(BAD_WORD_PATTERN, "");
126+
builder.addKeyword(badWord);
127+
}
128+
Trie badWordTrie = builder.build();
129+
130+
// 텍스트에서 특수문자만 제거하고, 공백은 그대로 유지 (공백 덕분에 단어가 분리됨)
131+
String sanitizedText = text.replaceAll(BAD_WORD_PATTERN, "");
122132

123-
if (sanitizedText.contains(sanitizedBadWord)) {
124-
throw new BadWordContainsException();
125-
}
133+
// 아호코라식 트리로 텍스트를 검사
134+
Collection<Emit> matches = badWordTrie.parseText(sanitizedText);
135+
// 금칙어가 발견되면 예외를 던짐
136+
if (!matches.isEmpty()) {
137+
throw new BadWordContainsException();
126138
}
127139
}
140+
141+
128142
}

src/main/java/io/crops/warmletter/domain/letter/service/LetterService.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import io.crops.warmletter.domain.letter.dto.request.CreateLetterRequest;
66
import io.crops.warmletter.domain.letter.dto.request.EvaluateLetterRequest;
77
import io.crops.warmletter.domain.letter.dto.request.TemporarySaveLetterRequest;
8-
import io.crops.warmletter.domain.letter.dto.response.LetterDraftResponse;
98
import io.crops.warmletter.domain.letter.dto.response.LetterResponse;
109
import io.crops.warmletter.domain.letter.entity.Letter;
1110
import io.crops.warmletter.domain.letter.entity.LetterMatching;
@@ -17,17 +16,17 @@
1716
import io.crops.warmletter.domain.member.exception.MemberNotFoundException;
1817
import io.crops.warmletter.domain.member.facade.MemberFacade;
1918
import io.crops.warmletter.domain.member.repository.MemberRepository;
19+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
2020
import io.crops.warmletter.domain.timeline.enums.AlarmType;
21-
import io.crops.warmletter.domain.timeline.facade.NotificationFacade;
2221
import io.crops.warmletter.global.error.exception.BusinessException;
2322
import lombok.RequiredArgsConstructor;
23+
import org.springframework.context.ApplicationEventPublisher;
2424
import org.springframework.stereotype.Service;
2525
import org.springframework.transaction.annotation.Transactional;
2626

2727
import java.util.ArrayList;
2828
import java.util.List;
2929
import java.util.Map;
30-
import java.util.Optional;
3130
import java.util.stream.Collectors;
3231

3332
import static io.crops.warmletter.global.error.common.ErrorCode.INVALID_INPUT_VALUE;
@@ -44,7 +43,7 @@ public class LetterService {
4443
private final MemberFacade memberFacade;
4544
private final AuthFacade authFacade;
4645

47-
private final NotificationFacade notificationFacade;
46+
private final ApplicationEventPublisher notificationPublisher;
4847

4948
@Transactional
5049
public LetterResponse createLetter(CreateLetterRequest request) {
@@ -109,7 +108,12 @@ public LetterResponse createLetter(CreateLetterRequest request) {
109108

110109
// 알림 전송
111110
if(request.getReceiverId() != null){
112-
notificationFacade.sendNotification(zipCode,request.getReceiverId(), AlarmType.SENDING,null);
111+
notificationPublisher.publishEvent(NotificationRequest.builder()
112+
.senderZipCode(zipCode)
113+
.receiverId(request.getReceiverId())
114+
.alarmType(AlarmType.SENDING)
115+
.data(null)
116+
.build());
113117
}
114118

115119
return LetterResponse.fromEntity(savedLetter, zipCode);

src/main/java/io/crops/warmletter/domain/report/service/ReportService.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
import io.crops.warmletter.domain.share.exception.ShareProposalNotFoundException;
2929
import io.crops.warmletter.domain.share.repository.SharePostRepository;
3030
import io.crops.warmletter.domain.share.repository.ShareProposalRepository;
31+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
3132
import io.crops.warmletter.domain.timeline.enums.AlarmType;
3233
import io.crops.warmletter.domain.timeline.facade.NotificationFacade;
3334
import jakarta.transaction.Transactional;
3435
import lombok.RequiredArgsConstructor;
36+
import org.springframework.context.ApplicationEventPublisher;
3537
import org.springframework.data.domain.Page;
3638
import org.springframework.data.domain.Pageable;
3739
import org.springframework.stereotype.Service;
@@ -56,7 +58,8 @@ public class ReportService {
5658
private final ReportModerationService reportModerationService;
5759

5860
private final AuthFacade authFacde;
59-
private final NotificationFacade notificationFacade;
61+
62+
private final ApplicationEventPublisher notificationPublisher;
6063

6164
@Transactional
6265
public UpdateReportResponse updateReport(Long reportId, UpdateReportRequest request) {
@@ -81,7 +84,12 @@ public UpdateReportResponse updateReport(Long reportId, UpdateReportRequest requ
8184
}
8285
resolvePendingReports(report);
8386
// targetMemberId로 알림 전송
84-
notificationFacade.sendNotification(null, targetMemberId, AlarmType.REPORT, report.getAdminMemo()+"§"+reportedMember.getWarningCount());
87+
notificationPublisher.publishEvent(NotificationRequest.builder()
88+
.senderZipCode(null)
89+
.receiverId(targetMemberId)
90+
.alarmType(AlarmType.REPORT)
91+
.data(report.getAdminMemo()+"§"+reportedMember.getWarningCount())
92+
.build());
8593
}
8694
return new UpdateReportResponse(report,reportedMember);
8795
}
@@ -142,7 +150,13 @@ public void updateReportWithAIResult(Long reportId, Map<String, String> moderati
142150
.orElseThrow(MemberNotFoundException::new);
143151
reportedMember.increaseWarningCount();
144152
memberRepository.save(reportedMember);
145-
notificationFacade.sendNotification(null, targetMemberId, AlarmType.REPORT, report.getAdminMemo()+"§"+reportedMember.getWarningCount());
153+
// 알림
154+
notificationPublisher.publishEvent(NotificationRequest.builder()
155+
.senderZipCode(null)
156+
.receiverId(targetMemberId)
157+
.alarmType(AlarmType.REPORT)
158+
.data(report.getAdminMemo()+"§"+reportedMember.getWarningCount())
159+
.build());
146160
}
147161
resolvePendingReports(report);
148162
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
package io.crops.warmletter.domain.share.repository;
22
import io.crops.warmletter.domain.share.entity.ShareProposal;
33
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.data.jpa.repository.Query;
45
import org.springframework.stereotype.Repository;
56

67
@Repository
78
public interface ShareProposalRepository extends JpaRepository<ShareProposal,Long >, ShareProposalRepositoryCustom {
8-
9+
@Query("SELECT m.zipCode " +
10+
"FROM ShareProposal proposal " +
11+
"JOIN Member m ON proposal.requesterId = m.id " +
12+
"WHERE proposal.requesterId = :requesterId")
913
String findZipCodeByRequesterId(Long requesterId);
14+
15+
@Query("SELECT m.zipCode " +
16+
"FROM ShareProposal proposal " +
17+
"JOIN Member m ON proposal.recipientId = m.id " +
18+
"WHERE proposal.recipientId = :recipientId")
19+
String findZipCodeByRecipientId(Long recipientId);
1020
}

src/main/java/io/crops/warmletter/domain/share/service/ShareProposalService.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import io.crops.warmletter.domain.share.exception.ShareAccessException;
1313
import io.crops.warmletter.domain.share.exception.ShareProposalNotFoundException;
1414
import io.crops.warmletter.domain.share.repository.*;
15+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
1516
import io.crops.warmletter.domain.timeline.enums.AlarmType;
1617
import io.crops.warmletter.domain.timeline.facade.NotificationFacade;
1718
import lombok.RequiredArgsConstructor;
19+
import org.springframework.context.ApplicationEventPublisher;
1820
import org.springframework.stereotype.Service;
1921
import org.springframework.transaction.annotation.Transactional;
2022
import java.util.List;
@@ -29,7 +31,7 @@ public class ShareProposalService {
2931
private final SharePostRepository sharePostRepository;
3032
private final AuthFacade authFacade;
3133

32-
private final NotificationFacade notificationFacade;
34+
private final ApplicationEventPublisher notificationPublisher;
3335

3436
@Transactional
3537
public ShareProposalResponse requestShareProposal(ShareProposalRequest request) {
@@ -48,15 +50,18 @@ public ShareProposalResponse requestShareProposal(ShareProposalRequest request)
4850
throw new ShareProposalNotFoundException();
4951
}
5052
// 알림 전송
51-
notificationFacade.sendNotification(response.getZipCode(), request.getRecipientId(), AlarmType.SHARE, response.getShareProposalId().toString());
53+
notificationPublisher.publishEvent(NotificationRequest.builder()
54+
.senderZipCode(response.getZipCode())
55+
.receiverId(request.getRecipientId())
56+
.alarmType(AlarmType.SHARE)
57+
.data(response.getShareProposalId().toString())
58+
.build());
5259
return response;
5360
}
5461

5562
@Transactional
5663
public ShareProposalStatusResponse approveShareProposal(Long shareProposalId) {
57-
5864
Long memberId = authFacade.getCurrentUserId();
59-
6065
ShareProposal shareProposal = shareProposalRepository.findById(shareProposalId)
6166
.orElseThrow(() -> new ShareProposalNotFoundException());
6267

@@ -73,11 +78,21 @@ public ShareProposalStatusResponse approveShareProposal(Long shareProposalId) {
7378
.isActive(true)
7479
.build();
7580
sharePost = sharePostRepository.save(sharePost);
76-
// 알림 전송(양쪽다)
81+
// 알림 전송(양쪽 다)
7782
String requestZipCode = shareProposalRepository.findZipCodeByRequesterId(shareProposal.getRequesterId());
78-
String recipientZipCode = shareProposalRepository.findZipCodeByRequesterId(shareProposal.getRecipientId());
79-
notificationFacade.sendNotification(recipientZipCode, shareProposal.getRequesterId(), AlarmType.POSTED, sharePost.getId().toString());
80-
notificationFacade.sendNotification(requestZipCode, shareProposal.getRecipientId(), AlarmType.POSTED, sharePost.getId().toString());
83+
String recipientZipCode = shareProposalRepository.findZipCodeByRecipientId(shareProposal.getRecipientId());
84+
notificationPublisher.publishEvent(NotificationRequest.builder()
85+
.senderZipCode(recipientZipCode)
86+
.receiverId(shareProposal.getRequesterId())
87+
.alarmType(AlarmType.POSTED)
88+
.data(sharePost.getId().toString())
89+
.build());
90+
notificationPublisher.publishEvent(NotificationRequest.builder()
91+
.senderZipCode(requestZipCode)
92+
.receiverId(shareProposal.getRecipientId())
93+
.alarmType(AlarmType.POSTED)
94+
.data(sharePost.getId().toString())
95+
.build());
8196
return ShareProposalStatusResponse.builder()
8297
.shareProposalId(shareProposal.getId())
8398
.status(shareProposal.getStatus())
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.crops.warmletter.domain.timeline.dto.request;
2+
3+
import io.crops.warmletter.domain.timeline.enums.AlarmType;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@Builder
9+
public class NotificationRequest {
10+
String senderZipCode;
11+
Long receiverId;
12+
AlarmType alarmType;
13+
String data;
14+
}
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
package io.crops.warmletter.domain.timeline.facade;
22

3+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
34
import io.crops.warmletter.domain.timeline.enums.AlarmType;
45
import io.crops.warmletter.domain.timeline.service.NotificationService;
56
import lombok.RequiredArgsConstructor;
7+
import org.springframework.scheduling.annotation.Async;
68
import org.springframework.stereotype.Component;
9+
import org.springframework.transaction.event.TransactionPhase;
10+
import org.springframework.transaction.event.TransactionalEventListener;
711

812
@Component
913
@RequiredArgsConstructor
1014
public class NotificationFacade {
1115

1216
private final NotificationService notificationService;
1317

14-
public void sendNotification(String senderZipCode, Long receiverId, AlarmType alarmType, String data) {
15-
notificationService.createNotification(senderZipCode, receiverId, alarmType, data);
18+
@Async
19+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
20+
public void sendNotification(NotificationRequest notificationRequest) {
21+
notificationService.createNotification(
22+
notificationRequest.getSenderZipCode(),
23+
notificationRequest.getReceiverId(),
24+
notificationRequest.getAlarmType(),
25+
notificationRequest.getData());
1626
}
1727
}

src/main/java/io/crops/warmletter/domain/timeline/service/NotificationService.java

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,7 @@ public class NotificationService {
2727
private final TimelineRepository timelineRepository;
2828

2929
public SseEmitter subscribeNotification(){
30-
Long memberId;
31-
try {
32-
memberId = authFacade.getCurrentUserId();
33-
} catch (UnauthorizedException e){
34-
log.warn("SSE 구독 실패: 인증되지 않은 사용자");
35-
SseEmitter emitter = new SseEmitter(0L);
36-
NotificationResponse notificationResponse = NotificationResponse.builder()
37-
.title("Unauthorized")
38-
.alarmType("TEST").build();
39-
try{
40-
emitter.send(SseEmitter.event()
41-
.data(notificationResponse));
42-
}catch (IOException ioException){
43-
log.warn("SSE 에러 전송 실패 - Unauthorized");
44-
}
45-
emitter.complete();
46-
return emitter;
47-
}
30+
Long memberId = authFacade.getCurrentUserId();
4831

4932
SseEmitter emitter = new SseEmitter(600_000L); // 10분 후 타임아웃 설정
5033

src/main/java/io/crops/warmletter/global/schedule/DeliverySchedule.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import io.crops.warmletter.domain.letter.enums.Status;
55
import io.crops.warmletter.domain.letter.repository.LetterRepository;
66
import io.crops.warmletter.domain.member.repository.MemberRepository;
7+
import io.crops.warmletter.domain.timeline.dto.request.NotificationRequest;
78
import io.crops.warmletter.domain.timeline.dto.response.LetterAlarmResponse;
89
import io.crops.warmletter.domain.timeline.enums.AlarmType;
910
import io.crops.warmletter.domain.timeline.facade.NotificationFacade;
1011
import jakarta.transaction.Transactional;
1112
import lombok.RequiredArgsConstructor;
1213
import lombok.extern.slf4j.Slf4j;
14+
import org.springframework.context.ApplicationEventPublisher;
1315
import org.springframework.context.annotation.Configuration;
1416
import org.springframework.scheduling.annotation.Scheduled;
1517

@@ -25,7 +27,8 @@
2527
public class DeliverySchedule {
2628

2729
private final LetterRepository letterRepository;
28-
private final NotificationFacade notificationFacade;
30+
31+
private final ApplicationEventPublisher notificationPublisher;
2932

3033
@Transactional
3134
@Scheduled(cron = "0 */1 * * * *", zone = "Asia/Seoul")
@@ -55,11 +58,12 @@ public void processDeliveryCompletion() {
5558
letter.updateStatus(Status.DELIVERED);
5659
log.info("편지 ID: {} 배송 완료 처리됨", letter.getId());
5760
// 도착 알림 전송
58-
notificationFacade.sendNotification(
59-
senderZipCodes.get(letter.getWriterId()),
60-
letter.getReceiverId(),
61-
AlarmType.LETTER,
62-
letter.getId().toString());
61+
notificationPublisher.publishEvent(NotificationRequest.builder()
62+
.senderZipCode(senderZipCodes.get(letter.getWriterId()))
63+
.receiverId(letter.getReceiverId())
64+
.alarmType(AlarmType.LETTER)
65+
.data(letter.getId().toString())
66+
.build());
6367
}
6468

6569
// 변경사항 저장

0 commit comments

Comments
 (0)