Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5ccdfac
feat: 분실물 게시글 키워드 알림 이벤트 클래스 추가
Soundbar91 May 8, 2026
c9264c6
feat: 분실물 게시글 키워드 알림 이벤트 리스너 추가
Soundbar91 May 8, 2026
196f0f6
feat: 분실물 게시글 키워드 추출 클래스 추가
Soundbar91 May 8, 2026
0914c09
fix: ArticleKeywordEvent 시그니처 수정
Soundbar91 May 8, 2026
fd783ba
fix: 키워드 알림, 분실물 키워드 알림 생성 로직 분리
Soundbar91 May 8, 2026
bbf9c26
fix: ArticleKeywordEventListene 미사용 로직 삭제
Soundbar91 May 8, 2026
48c3e62
fix: ArticleKeywordEventListene 미사용 로직 삭제
Soundbar91 May 8, 2026
c559974
feat: LostItemKeywordEventListener 추가
Soundbar91 May 8, 2026
cf5a468
fix: 이벤트 키워드 알림 payload 변경
Soundbar91 May 8, 2026
e1391cd
refactor: 키워드 매칭 사용자 조회 책임 이동
Soundbar91 May 8, 2026
0b18752
refactor: 키워드 알림 발송 책임 서비스로 이동
Soundbar91 May 8, 2026
5684531
refactor: 키워드 이벤트 리스너 위임만 수행
Soundbar91 May 8, 2026
ea084bf
fix: 방어로직 삭제
Soundbar91 May 8, 2026
3b742a1
fix: 게시글 중복처리 로직 수정
Soundbar91 May 8, 2026
513a60b
fix: KoreatechArticleKeywordEvent 수정
Soundbar91 May 8, 2026
297644d
refactor: 키워드 매칭 사용자 선정 로직 정리
Soundbar91 May 8, 2026
5a6e43c
refactor: 키워드 알림 서비스 로직 정리
Soundbar91 May 8, 2026
572c9b6
refactor: 분실물 키워드 알림 책임 분리
Soundbar91 May 8, 2026
31267b1
refactor: 키워드 추출기 통합
Soundbar91 May 8, 2026
3b35510
refactor: 키워드 사용자 매칭 로직 분리
Soundbar91 May 8, 2026
71f2990
test: 잘못된 키워드 알림 테스트 제거
Soundbar91 May 8, 2026
2319bdf
refactor: 미사용 키워드 메서드 제거
Soundbar91 May 8, 2026
52f9ede
refactor: 미사용 키워드 필드 제거
Soundbar91 May 8, 2026
ec1927f
fix: 미사용 메소드 삭제
Soundbar91 May 8, 2026
9dddac7
feat: Profile 추가
Soundbar91 May 8, 2026
9179ca1
fix: keyword 하드 코딩 수정
Soundbar91 May 8, 2026
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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package in.koreatech.koin.common.event;

import java.util.List;
import java.util.Map;

public record KoreatechArticleKeywordEvent(
Integer articleId,
Integer boardId,
String articleTitle,
MatchedKeywordUsers matchedKeywordUsers
Comment thread
Soundbar91 marked this conversation as resolved.
) {
public record MatchedKeywordUsers(
Map<String, List<Integer>> userIdsByKeyword
) {

}

public static KoreatechArticleKeywordEvent of(
Integer articleId,
Integer boardId,
String articleTitle,
Map<String, List<Integer>> userIdsByKeyword
) {
return new KoreatechArticleKeywordEvent(
articleId,
boardId,
articleTitle,
new MatchedKeywordUsers(userIdsByKeyword)
);
}
Comment on lines +18 to +30
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package in.koreatech.koin.common.event;

import java.util.List;
import java.util.Map;

public record LostItemKeywordEvent(
Integer articleId,
String articleTitle,
Integer authorId,
MatchedKeywordUsers matchedKeywordUsers
) {
public record MatchedKeywordUsers(
Map<String, List<Integer>> userIdsByKeyword
) {

}

public static LostItemKeywordEvent of(
Integer articleId,
String articleTitle,
Integer authorId,
Map<String, List<Integer>> userIdsByKeyword
) {
return new LostItemKeywordEvent(
articleId,
articleTitle,
authorId,
new MatchedKeywordUsers(userIdsByKeyword)
);
}
Comment on lines +18 to +30
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static in.koreatech.koin.domain.community.article.service.ArticleService.NOTICE_BOARD_ID;

import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

Expand All @@ -29,7 +30,7 @@ public interface ArticleRepository extends Repository<Article, Integer> {

Optional<Article> findById(Integer articleId);

List<Article> findAllByIdIn(List<Integer> articleIds);
List<Article> findAllByIdIn(Collection<Integer> articleIds);

Page<Article> findAll(Pageable pageable);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
Expand All @@ -14,7 +15,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import in.koreatech.koin.common.event.ArticleKeywordEvent;
import in.koreatech.koin.common.event.LostItemKeywordEvent;
import in.koreatech.koin.common.model.Criteria;
import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponse;
import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponseV2;
Expand All @@ -27,14 +28,15 @@
import in.koreatech.koin.domain.community.article.model.Board;
import in.koreatech.koin.domain.community.article.model.LostItemArticle;
import in.koreatech.koin.domain.community.article.model.filter.LostItemAuthorFilter;
import in.koreatech.koin.domain.community.article.model.filter.LostItemFoundStatus;
import in.koreatech.koin.domain.community.article.model.filter.LostItemCategoryFilter;
import in.koreatech.koin.domain.community.article.model.filter.LostItemFoundStatus;
import in.koreatech.koin.domain.community.article.model.filter.LostItemSortType;
import in.koreatech.koin.domain.community.article.model.redis.PopularKeywordTracker;
import in.koreatech.koin.domain.community.article.repository.ArticleRepository;
import in.koreatech.koin.domain.community.article.repository.BoardRepository;
import in.koreatech.koin.domain.community.article.repository.LostItemArticleRepository;
import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory;
import in.koreatech.koin.domain.community.keyword.service.ArticleKeywordUserMatcher;
import in.koreatech.koin.domain.community.util.KeywordExtractor;
import in.koreatech.koin.domain.organization.model.Organization;
import in.koreatech.koin.domain.organization.repository.OrganizationRepository;
Expand Down Expand Up @@ -67,6 +69,7 @@ public class LostItemArticleService {
private final PopularKeywordTracker popularKeywordTracker;
private final ApplicationEventPublisher eventPublisher;
private final KeywordExtractor keywordExtractor;
private final ArticleKeywordUserMatcher articleKeywordUserMatcher;

@Transactional
public LostItemArticlesResponse searchLostItemArticles(String query, Integer page, Integer limit,
Expand Down Expand Up @@ -253,11 +256,26 @@ private Board getBoard(Integer boardId, Article article) {
}

private void sendKeywordNotification(List<Article> articles, Integer authorId) {
List<ArticleKeywordEvent> keywordEvents = keywordExtractor.matchKeyword(articles, authorId, KeywordCategory.LOST_ITEM);
if (!keywordEvents.isEmpty()) {
for (ArticleKeywordEvent event : keywordEvents) {
eventPublisher.publishEvent(event);
for (Article article : articles) {
List<String> matchedKeywords = keywordExtractor.matchKeywords(article.getTitle(), KeywordCategory.LOST_ITEM);
if (matchedKeywords.isEmpty()) {
continue;
}

Map<String, List<Integer>> userIdsByKeyword = articleKeywordUserMatcher.findUserIdsByMatchedKeyword(
KeywordCategory.LOST_ITEM,
matchedKeywords
);
if (userIdsByKeyword.isEmpty()) {
continue;
}

eventPublisher.publishEvent(LostItemKeywordEvent.of(
article.getId(),
article.getTitle(),
authorId,
userIdsByKeyword
));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package in.koreatech.koin.domain.community.keyword.dto;

import java.util.List;
import java.util.Set;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
Expand All @@ -9,10 +9,10 @@
import jakarta.validation.constraints.NotNull;

@JsonNaming(SnakeCaseStrategy.class)
public record KeywordNotificationRequest (
public record KeywordNotificationRequest(
@Schema(description = "업데이트 된 공지사항 목록", example = "[1, 2, 3]")
@NotNull
List<Integer> updateNotification
Set<Integer> updateNotification
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ public void applyFiltered(Boolean isFiltered) {
this.isFiltered = isFiltered;
}

public boolean hasLongerKeywordThan(ArticleKeyword other) {
return other == null || compareKeywordLength(other) > 0;
}

private int compareKeywordLength(ArticleKeyword other) {
return Integer.compare(keyword.length(), other.keyword.length());
}

public void delete() {
this.isDeleted = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ Optional<ArticleKeyword> findByKeywordAndCategoryIncludingDeleted(

ArticleKeyword save(ArticleKeyword articleKeyword);

void deleteById(Integer id);

Optional<ArticleKeyword> findById(Integer id);

List<ArticleKeyword> findAllByCategory(KeywordCategory category, Pageable pageable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ public interface ArticleKeywordUserMapRepository extends Repository<ArticleKeywo

Long countByUserIdAndArticleKeywordCategory(Integer userId, KeywordCategory category);

void deleteById(Integer keywordId);

boolean existsByArticleKeywordId(Integer id);

Optional<ArticleKeywordUserMap> findById(Integer keywordUserMapId);
Expand All @@ -30,17 +28,6 @@ default ArticleKeywordUserMap getById(Integer keywordUserMapId) {

List<ArticleKeywordUserMap> findAllByUserIdAndArticleKeywordCategory(Integer userId, KeywordCategory category);

@Query("""
SELECT akw.keyword FROM ArticleKeywordUserMap akum
JOIN akum.articleKeyword akw
WHERE akum.user.id = :userId
AND akw.category = :category
""")
List<String> findAllKeywordByUserIdAndCategory(
@Param("userId") Integer userId,
@Param("category") KeywordCategory category
);

@Query(value = """
SELECT * FROM article_keyword_user_map akum
WHERE akum.keyword_id = :articleKeywordId
Expand All @@ -51,5 +38,17 @@ Optional<ArticleKeywordUserMap> findByArticleKeywordIdAndUserIdIncludingDeleted(
@Param("userId") Integer userId
);

List<ArticleKeywordUserMap> findAllByArticleKeywordIdIn(List<Integer> articleKeywordIds);
@Query("""
SELECT akum
FROM ArticleKeywordUserMap akum
JOIN FETCH akum.articleKeyword akw
JOIN FETCH akum.user
WHERE akw.category = :category
AND akw.keyword IN :keywords
AND akum.isDeleted = false
""")
List<ArticleKeywordUserMap> findAllByArticleKeywordCategoryAndArticleKeywordKeywordIn(
@Param("category") KeywordCategory category,
@Param("keywords") List<String> keywords
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package in.koreatech.koin.domain.community.keyword.service;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import in.koreatech.koin.domain.community.keyword.enums.KeywordCategory;
import in.koreatech.koin.domain.community.keyword.model.ArticleKeyword;
import in.koreatech.koin.domain.community.keyword.model.ArticleKeywordUserMap;
import in.koreatech.koin.domain.community.keyword.repository.ArticleKeywordUserMapRepository;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ArticleKeywordUserMatcher {

private final ArticleKeywordUserMapRepository articleKeywordUserMapRepository;

public Map<String, List<Integer>> findUserIdsByMatchedKeyword(
KeywordCategory category,
List<String> matchedKeywords
) {
Map<Integer, ArticleKeyword> keywordByUserId = new LinkedHashMap<>();
List<ArticleKeywordUserMap> keywordUserMaps = articleKeywordUserMapRepository
.findAllByArticleKeywordCategoryAndArticleKeywordKeywordIn(category, matchedKeywords);

for (ArticleKeywordUserMap keywordUserMap : keywordUserMaps) {
Integer userId = keywordUserMap.getUser().getId();
ArticleKeyword keyword = keywordUserMap.getArticleKeyword();
ArticleKeyword previousKeyword = keywordByUserId.get(userId);

if (keyword.hasLongerKeywordThan(previousKeyword)) {
keywordByUserId.put(userId, keyword);
}
}

Map<String, List<Integer>> userIdsByKeyword = new LinkedHashMap<>();
for (Map.Entry<Integer, ArticleKeyword> entry : keywordByUserId.entrySet()) {
Integer userId = entry.getKey();
String keyword = entry.getValue().getKeyword();
userIdsByKeyword.computeIfAbsent(keyword, ignored -> new ArrayList<>()).add(userId);
}
return userIdsByKeyword;
}
}
Loading
Loading