Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
2bbe0d6
feat: Feed 도메인 이벤트 구조 추가
jj0526 Feb 8, 2026
5e78777
feat: 알림 발행을 위한 Port 인터페이스 및 DTO 추가
jj0526 Feb 8, 2026
e957861
feat: 알림 발행 정책 및 Publisher 중앙화
jj0526 Feb 8, 2026
3a186b7
feat: 코루틴 기반 NotificationService 구현
jj0526 Feb 8, 2026
f9cae6f
feat: Feed 도메인 이벤트를 알림으로 변환하는 어댑터 추가
jj0526 Feb 8, 2026
fd0b3eb
refactor: Notification 엔티티 구조 단순화
jj0526 Feb 8, 2026
78df67d
feat: getContent 메소드 추가
jj0526 Feb 8, 2026
a4713ff
refactor: FeedUsecase에서 직접 호출을 이벤트 발행으로 전환
jj0526 Feb 8, 2026
4c3d8ed
refactor: NotificationType 코틀린 마이그레이션
jj0526 Feb 8, 2026
2756c0b
refactor: NotificationType 디렉토리 위치 변경
jj0526 Feb 8, 2026
7ba0535
refactor: SqsMessageEvent 생성자 생성
jj0526 Feb 8, 2026
b0a2cb1
refactor: markNotificationAsRead() 메서드 notificationUseCase로 이동
jj0526 Feb 9, 2026
af84645
feat: Feed 도메인 이벤트에 previousReactionCount 추가
jj0526 Feb 9, 2026
1f4fa72
refactor: NotificationEntityContent 삭제
jj0526 Feb 9, 2026
e973728
refactor: FEED_REACTION_COUNT 알림 중복 체크 및 집계 로직 개선
jj0526 Feb 9, 2026
e1ad234
알림 로직 개선
jj0526 Feb 9, 2026
01d2b01
refactor: 사용하지 않는 주석 제거 및 리팩토링
jj0526 Feb 9, 2026
8a8687a
feat: sendOrUpdate 메서드 추가
jj0526 Feb 9, 2026
cb055bd
refactor: ktlint 포맷으로 변경
jj0526 Feb 9, 2026
7760fdc
Merge branch 'dev' into LNK-71-Leenk-domain-notification-코틀린-마이그레이션
jj0526 Feb 9, 2026
23f21ef
refactor: 알림 시스템을 어댑터 패턴 기반 이벤트 구조로 전환
jj0526 Feb 9, 2026
1cee64a
chore: gradle.properties 삭제
jj0526 Feb 9, 2026
0a2450e
refactor: FeedUsecaseTest에 eventPublisher 추가
jj0526 Feb 9, 2026
0ecce79
test: 알림 저장 검증을 이벤트 발행 검증으로 변경
jj0526 Feb 11, 2026
2c27215
refactor: FeedUseCase.java 삭제
jj0526 Feb 11, 2026
904a626
refactor: FeedUseCase에서 feedNotificationUsecase 의존성 삭제
jj0526 Feb 11, 2026
135491a
refactor: NotificationController에서 미사용 feedNotificationUsecase 삭제
jj0526 Feb 11, 2026
16b3f1a
refactor: 리플렉션 제거후 user.getToken으로 변경
jj0526 Feb 11, 2026
6cbcb68
refactor: NotificationType.formatContent()를 named parameter 방식으로 변경
jj0526 Feb 11, 2026
18099c3
feat: UserSetting 기반 알림 필터링 구현
jj0526 Feb 11, 2026
5172409
test: 알림에 테스트 추가
jj0526 Feb 11, 2026
29eb1e5
refactor: FeedNotificationAdapter -> FeedNotificationEventListener로 클…
jj0526 Feb 11, 2026
b7c8449
refactor: e.printStackTrace()를 SLF4J 로거로 변경
jj0526 Feb 11, 2026
a12e2a9
refactor: 사용자 알림 설정 중복 체크 로직 제거
jj0526 Feb 11, 2026
80e8843
refactor: 알림 발행 로그 추가 (NotificationPublisher)
jj0526 Feb 11, 2026
7be63af
refactor: 알림 마일스톤 검사 로직을 구조화된 필드 기반으로 개선
jj0526 Feb 11, 2026
1e83fe0
refactor: 마이그레이션 완료된 파일 사용하지 않는 메소드 삭제
jj0526 Feb 11, 2026
b8dde76
refactor: 알림 저장 트랜잭션 처리하도록 수정
jj0526 Feb 11, 2026
6a8f274
style: 개행추가
jj0526 Feb 11, 2026
7ef2c53
refactor: FeedDomainEvent를 sealed class로 변경
jj0526 Feb 12, 2026
5bd6876
refactor: UserSettingGetService import 추가
jj0526 Feb 12, 2026
8194de3
refactor: NotificationEntity에 deleteDate 추가
jj0526 Feb 12, 2026
d585c83
refactor: Feed 공감 이벤트의 totalReactionCount 계산 로직 개선
jj0526 Feb 12, 2026
cddb6d6
refactor: MongoDB 알림 처리 로직 개선 및 동시성 이슈 해결
jj0526 Feb 12, 2026
6bae775
refactor: sendBatch()에 푸시 알림 발송 추가
jj0526 Feb 12, 2026
c2fa607
refactor: sendOrUpdateWithMultiplePush 메서드로 DB 1회 저장, 푸시 개별 발행 지원
jj0526 Feb 12, 2026
74559ae
refactor: SQS 이벤트 리스너 트랜잭션 처리 방식 변경
jj0526 Feb 12, 2026
72e3936
fix: 알림 발행 시 경로 참조 위치 수정 (notificationType -> content)
jj0526 Feb 12, 2026
e215a3c
feat: 알림 서비스 예외 처리에 runCatching 추가
jj0526 Feb 12, 2026
8780959
refactor: suspend 함수와 트랜잭션 분리하여 코루틴 안정성 개선
jj0526 Feb 12, 2026
e51b6fe
test: 이벤트 발행 검증을 구체적인 타입으로 개선
jj0526 Feb 12, 2026
98c051a
test: 코루틴 기반 테스트 수정
jj0526 Feb 12, 2026
dbfd30d
test: sendOrUpdate 실패 시나리오 테스트 추가
jj0526 Feb 12, 2026
3ce17aa
test: NotificationServiceTest에서 runBlocking 제거 후 suspend delay로 교체
jj0526 Feb 22, 2026
16cdcdf
refactor: Race Condition 발생 시 pushDetails 반환 방식을 명시적으로 변경
jj0526 Feb 23, 2026
0d291b2
refactor: 마일스톤 필터 조건을 범위 검사로 변경
jj0526 Feb 23, 2026
5da3012
refactor: count 타입을 Any?에서 Long?으로 변경
jj0526 Feb 23, 2026
4697206
refactor: markNotificationAsRead를 markAsRead로 메소드명 변경
jj0526 Feb 23, 2026
11b5758
Merge branch 'dev' into LNK-71-Leenk-domain-notification-코틀린-마이그레이션
jj0526 Feb 23, 2026
1880574
refactor: !! 단언 제거 및 requireId 함수로 명시적 예외 처리
jj0526 Feb 23, 2026
1027e22
refactor: mongoTemplate 호출을 Kotlin 확장 함수로 변경
jj0526 Feb 23, 2026
28c2769
refactor: NotificationPublisher를 아웃바운드 포트 구조로 개선
jj0526 Feb 23, 2026
ff92ce1
style: ktlint 코드 스타일 오류 수정
jj0526 Feb 23, 2026
7334555
test: reactionCount 타입 Long으로 변경
jj0526 Feb 23, 2026
871db61
test: FeedNotificationEventListenerTest 및 FeedDomainEventFixture 추가
jj0526 Feb 23, 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
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

// Kotlin Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.0")

// Spring
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public FeedFirstReactionDetail toFeedFirstReactionDetail(User user) {
.userId(user.getId())
.name(user.getName())
.title(NotificationType.FEED_FIRST_REACTION.getTitle())
.body(NotificationType.FEED_FIRST_REACTION.getContent())
.body(NotificationType.FEED_FIRST_REACTION.formatContent(user.getName()))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public FeedReactionCountDetail toFeedReactionCountDetail(Long reactionCount) {
return FeedReactionCountDetail.builder()
.reactionCount(reactionCount)
.title(NotificationType.FEED_REACTION_COUNT.getTitle())
.body(NotificationType.FEED_REACTION_COUNT.getFormattedContent(reactionCount))
.body(NotificationType.FEED_REACTION_COUNT.formatContent(reactionCount))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,4 @@ public void saveTagNotification(Feed feed, List<LinkedUser> linkedUsers, User au
}
});
}

@Transactional
public void markNotificationAsRead(Long userId, String notificationId) {
User user = userGetService.findById(userId);
notificationMarkReadService.markReadNotification(user, notificationId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import leets.leenk.domain.notification.domain.entity.Notification;
import leets.leenk.domain.notification.domain.service.NotificationCountGetService;
import leets.leenk.domain.notification.domain.service.NotificationGetService;
import leets.leenk.domain.notification.domain.service.NotificationMarkReadService;
import leets.leenk.domain.user.domain.entity.User;
import leets.leenk.domain.user.domain.service.user.UserGetService;
import lombok.RequiredArgsConstructor;
Expand All @@ -22,6 +23,7 @@ public class NotificationUseCase {

private final UserGetService userGetService;
private final NotificationGetService notificationGetService;
private final NotificationMarkReadService notificationMarkReadService;

private final NotificationCountGetService notificationCountGetService;
private final NotificationResponseMapper notificationResponseMapper;
Expand All @@ -39,4 +41,10 @@ public NotificationCountResponse getNotificationCount(long userId) {
User user = userGetService.findById(userId);
return notificationResponseMapper.toCountResponse(notificationCountGetService.getNotificationCount(user));
}

@Transactional
public void markNotificationAsRead(Long userId, String notificationId) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

클래스명에서 이미 Notification이라는 문맥을 알 수 있으니 메서드명에서는 중복을 제거하고 markAsRead로 간결하게 줄이는 게 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

좋습니다

User user = userGetService.findById(userId);
notificationMarkReadService.markReadNotification(user, notificationId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ public void markRead() {
this.isRead = true;
}

public NotificationContent getContent() {
return this.content;
}

}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public CommonResponse<NotificationCountResponse> getNotificationCount(@Parameter
@PatchMapping("/{notificationId}")
public CommonResponse<Void> markAsRead(@Parameter(hidden = true) @CurrentUserId Long userId,
@PathVariable String notificationId) {
feedNotificationUsecase.markNotificationAsRead(userId, notificationId);
notificationUseCase.markNotificationAsRead(userId, notificationId);
return CommonResponse.success(NotificationResponseCode.NOTIFICATION_MARK_AS_READ_SUCCESS);
}
}
5 changes: 5 additions & 0 deletions src/main/java/leets/leenk/domain/user/domain/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,9 @@ public void reRegisterFromApple(String name) {
public boolean isAgree() {
return this.termsAgreement && this.privacyAgreement;
}

// TODO: 코틀린 자바 롬복 문제. 추후 마이그레이션 후 삭제
public Long getId() {
return id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@ public class SqsMessageEvent {
private final String path;
private final Long id;

public SqsMessageEvent(String title, String content, String fcmToken, String path, Long id) {
this.title = title;
this.content = content;
this.fcmToken = fcmToken;
this.path = path;
this.id = id;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import leets.leenk.domain.feed.application.mapper.ReactionMapper
import leets.leenk.domain.feed.domain.entity.Comment
import leets.leenk.domain.feed.domain.entity.Feed
import leets.leenk.domain.feed.domain.entity.LinkedUser
import leets.leenk.domain.feed.domain.event.FeedDomainEvent
import leets.leenk.domain.feed.domain.service.CommentDeleteService
import leets.leenk.domain.feed.domain.service.CommentGetService
import leets.leenk.domain.feed.domain.service.CommentSaveService
Expand All @@ -44,6 +45,7 @@ import leets.leenk.domain.user.domain.service.NotionDatabaseService
import leets.leenk.domain.user.domain.service.SlackWebhookService
import leets.leenk.domain.user.domain.service.blockuser.UserBlockService
import leets.leenk.domain.user.domain.service.user.UserGetService
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand Down Expand Up @@ -75,6 +77,7 @@ class FeedUsecase(
private val feedUserMapper: FeedUserMapper,
private val reactionMapper: ReactionMapper,
private val commentMapper: CommentMapper,
private val eventPublisher: ApplicationEventPublisher,
) {
@Transactional(readOnly = true)
fun getFeeds(
Expand Down Expand Up @@ -211,8 +214,17 @@ class FeedUsecase(
val linkedUsers = getLinkedUsers(author, request.userIds, feed)
linkedUserSaveService.saveAll(linkedUsers)

feedNotificationUsecase.saveNewFeedNotification(feed)
feedNotificationUsecase.saveTagNotification(feed, linkedUsers, author)
// 태그된 사용자 ID 목록 (작성자 제외)
val taggedUserIds = request.userIds.filter { it != author.id }

eventPublisher.publishEvent(
FeedDomainEvent.created(
feedId = feed.id!!,
authorId = author.id!!,
authorName = author.name,
taggedUserIds = taggedUserIds,
),
Comment on lines 219 to 224
Copy link
Collaborator

Choose a reason for hiding this comment

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

저도 피드백 받았는데 코틀린에서 단언은 지양한다고 합니당
feedId = feed.id ?: throw IllegalStateException("영속화되지 않은 Feed입니다."),로 예외를 던진다로 변경하는게 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵! 좋습니다 형식은 수현님이 해두신 형식 따라가겠습니다

)
}

private fun getLinkedUsers(
Expand Down Expand Up @@ -258,7 +270,20 @@ class FeedUsecase(

// Feed를 가져올 때 Fetch Join으로 작성자를 함께 가져와 락이 함께 걸리므로 별도의 락 필요 없음.
feedUpdateService.updateTotalReaction(feed, reaction, feed.user, request.reactionCount)
feedNotificationUsecase.saveFirstReactionNotification(reaction)

// 업데이트된 총 공감 수 계산
val totalReactionCount = feed.totalReactionCount + request.reactionCount

eventPublisher.publishEvent(
FeedDomainEvent.reacted(
feedId = feed.id!!,
feedAuthorId = feed.user.id!!,
reactorId = user.id!!,
reactorName = user.name,
previousReactionCount = previousReactionCount,
totalReactionCount = totalReactionCount,
),
)

val updatedReactionCount = previousReactionCount + request.reactionCount
notifyIfReachedReactionMilestone(previousReactionCount, updatedReactionCount, feed)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package leets.leenk.domain.feed.domain.event

/**
* Feed 도메인의 단일 이벤트
* eventType으로 이벤트 종류 구분, data로 필요한 정보 전달
*/
data class FeedDomainEvent(
val eventType: FeedEventType,
val data: Map<String, Any>,
) {
companion object {
/**
* 피드 생성 이벤트
*/
@JvmStatic
fun created(
feedId: Long,
authorId: Long,
authorName: String,
taggedUserIds: List<Long> = emptyList(),
) = FeedDomainEvent(
eventType = FeedEventType.CREATED,
data =
mapOf(
"feedId" to feedId,
"authorId" to authorId,
"authorName" to authorName,
"taggedUserIds" to taggedUserIds,
),
)

/**
* 피드 공감 이벤트
*/
@JvmStatic
fun reacted(
feedId: Long,
feedAuthorId: Long,
reactorId: Long,
reactorName: String,
previousReactionCount: Long,
totalReactionCount: Long,
) = FeedDomainEvent(
eventType = FeedEventType.REACTED,
data =
mapOf(
"feedId" to feedId,
"feedAuthorId" to feedAuthorId,
"reactorId" to reactorId,
"reactorName" to reactorName,
"previousReactionCount" to previousReactionCount,
"totalReactionCount" to totalReactionCount,
),
)
}

// 타입 안전한 접근자
val feedId: Long get() = data["feedId"] as Long
val authorId: Long get() = data["authorId"] as Long
val authorName: String get() = data["authorName"] as String
val taggedUserIds: List<Long>
get() = (data["taggedUserIds"] as? List<*>)?.filterIsInstance<Long>() ?: emptyList()
val feedAuthorId: Long get() = data["feedAuthorId"] as Long
val reactorId: Long get() = data["reactorId"] as Long
val reactorName: String get() = data["reactorName"] as String
val previousReactionCount: Long get() = data["previousReactionCount"] as Long
val totalReactionCount: Long get() = data["totalReactionCount"] as Long
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package leets.leenk.domain.feed.domain.event

/**
* Feed 도메인 이벤트 타입
*/
enum class FeedEventType {
CREATED, // 피드 생성
REACTED, // 피드 공감
}
Loading
Loading