Skip to content

Commit e5ddc46

Browse files
authored
Merge pull request #313 from DevKor-github/feat/event-listener
feat(community): 둥지 메인에서 자유게시판 노출 5개로 수정
2 parents 563c589 + 7472fc6 commit e5ddc46

24 files changed

Lines changed: 517 additions & 218 deletions

src/main/java/org/devkor/apu/saerok_server/domain/collection/application/BirdIdSuggestionCommandService.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
package org.devkor.apu.saerok_server.domain.collection.application;
22

3-
import jakarta.transaction.Transactional;
43
import lombok.RequiredArgsConstructor;
54
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.*;
5+
import org.devkor.apu.saerok_server.domain.collection.application.event.CollectionNotificationEvent;
66
import org.devkor.apu.saerok_server.domain.collection.core.entity.*;
77
import org.devkor.apu.saerok_server.domain.collection.core.repository.*;
88
import org.devkor.apu.saerok_server.domain.dex.bird.core.entity.Bird;
99
import org.devkor.apu.saerok_server.domain.dex.bird.core.repository.BirdRepository;
10-
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.ActionKind;
11-
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Actor;
12-
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
13-
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Target;
1410
import org.devkor.apu.saerok_server.domain.admin.stat.application.BirdIdRequestHistoryRecorder;
1511
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
1612
import org.devkor.apu.saerok_server.domain.user.core.repository.UserRepository;
1713
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
1814
import org.devkor.apu.saerok_server.global.shared.exception.ForbiddenException;
1915
import org.devkor.apu.saerok_server.global.shared.exception.NotFoundException;
16+
import org.springframework.context.ApplicationEventPublisher;
2017
import org.springframework.stereotype.Service;
18+
import org.springframework.transaction.annotation.Transactional;
2119

2220
import java.time.OffsetDateTime;
2321

@@ -30,8 +28,8 @@ public class BirdIdSuggestionCommandService {
3028
private final CollectionRepository collectionRepo;
3129
private final BirdRepository birdRepo;
3230
private final UserRepository userRepo;
33-
private final NotifyActionDsl notifyAction;
3431
private final BirdIdRequestHistoryRecorder birdReqHistory;
32+
private final ApplicationEventPublisher eventPublisher;
3533

3634
public SuggestBirdIdResponse suggest(Long userId, Long collectionId, Long birdId) {
3735
User user = userRepo.findById(userId)
@@ -88,12 +86,11 @@ public SuggestBirdIdResponse suggest(Long userId, Long collectionId, Long birdId
8886
}
8987
// 최초 제안인 경우에만 알림 발송
9088
if (!birdAlreadySuggested) {
91-
notifyAction
92-
.by(Actor.of(userId, user.getNickname()))
93-
.on(Target.collection(collectionId))
94-
.did(ActionKind.SUGGEST_BIRD_ID)
95-
.suggestedName(bird.getName().getKoreanName())
96-
.to(collection.getUser().getId());
89+
eventPublisher.publishEvent(new CollectionNotificationEvent.BirdIdSuggested(
90+
userId, user.getNickname(),
91+
collectionId, collection.getUser().getId(),
92+
bird.getName().getKoreanName()
93+
));
9794
}
9895

9996
return new SuggestBirdIdResponse(suggestionId);

src/main/java/org/devkor/apu/saerok_server/domain/collection/application/CollectionCommandService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.devkor.apu.saerok_server.domain.collection.application;
22

3-
import jakarta.transaction.Transactional;
43
import lombok.RequiredArgsConstructor;
54
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.UpdateCollectionResponse;
65
import org.devkor.apu.saerok_server.domain.collection.application.dto.CreateCollectionCommand;
@@ -26,6 +25,7 @@
2625
import org.devkor.apu.saerok_server.global.shared.infra.ImageService;
2726
import org.locationtech.jts.geom.Point;
2827
import org.springframework.stereotype.Service;
28+
import org.springframework.transaction.annotation.Transactional;
2929

3030
import java.time.OffsetDateTime;
3131
import java.util.List;

src/main/java/org/devkor/apu/saerok_server/domain/collection/application/CollectionCommentCommandService.java

Lines changed: 11 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
package org.devkor.apu.saerok_server.domain.collection.application;
22

3-
import jakarta.transaction.Transactional;
43
import lombok.RequiredArgsConstructor;
54
import org.devkor.apu.saerok_server.domain.collection.api.dto.request.CreateCollectionCommentRequest;
65
import org.devkor.apu.saerok_server.domain.collection.api.dto.request.UpdateCollectionCommentRequest;
76
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.CreateCollectionCommentResponse;
87
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.UpdateCollectionCommentResponse;
8+
import org.devkor.apu.saerok_server.domain.collection.application.event.CollectionNotificationEvent;
99
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
1010
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionComment;
1111
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionCommentRepository;
1212
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionRepository;
13-
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.ActionKind;
14-
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Actor;
15-
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
16-
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Target;
1713
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
1814
import org.devkor.apu.saerok_server.domain.user.core.repository.UserRepository;
1915
import org.devkor.apu.saerok_server.global.shared.exception.ForbiddenException;
2016
import org.devkor.apu.saerok_server.global.shared.exception.NotFoundException;
17+
import org.springframework.context.ApplicationEventPublisher;
2118
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
2220

2321
@Service
2422
@Transactional
@@ -28,7 +26,7 @@ public class CollectionCommentCommandService {
2826
private final CollectionCommentRepository commentRepository;
2927
private final CollectionRepository collectionRepository;
3028
private final UserRepository userRepository;
31-
private final NotifyActionDsl notifyAction;
29+
private final ApplicationEventPublisher eventPublisher;
3230

3331
/* 댓글 작성 */
3432
public CreateCollectionCommentResponse createComment(Long userId,
@@ -70,39 +68,13 @@ public CreateCollectionCommentResponse createComment(Long userId,
7068
commentRepository.save(comment);
7169

7270
// 알림 전송
73-
if (parentComment != null) {
74-
// 대댓글인 경우
75-
// 1) 원댓글 작성자에게 REPLY 알림
76-
if (!parentComment.getUser().getId().equals(userId)) {
77-
notifyAction
78-
.by(Actor.of(userId, user.getNickname()))
79-
.on(Target.comment(parentComment.getId()))
80-
.did(ActionKind.REPLY)
81-
.comment(req.content())
82-
.to(parentComment.getUser().getId());
83-
}
84-
85-
// 2) 컬렉션 소유자에게 COMMENT 알림 (원댓글 작성자와 다른 경우에만)
86-
if (!collection.getUser().getId().equals(userId)
87-
&& !collection.getUser().getId().equals(parentComment.getUser().getId())) {
88-
notifyAction
89-
.by(Actor.of(userId, user.getNickname()))
90-
.on(Target.collection(collectionId))
91-
.did(ActionKind.COMMENT)
92-
.comment(req.content())
93-
.to(collection.getUser().getId());
94-
}
95-
} else {
96-
// 원댓글인 경우
97-
if (!collection.getUser().getId().equals(userId)) {
98-
notifyAction
99-
.by(Actor.of(userId, user.getNickname()))
100-
.on(Target.collection(collectionId))
101-
.did(ActionKind.COMMENT)
102-
.comment(req.content())
103-
.to(collection.getUser().getId());
104-
}
105-
}
71+
eventPublisher.publishEvent(new CollectionNotificationEvent.CommentCreated(
72+
userId, user.getNickname(),
73+
collectionId, collection.getUser().getId(),
74+
parentComment != null ? parentComment.getId() : null,
75+
parentComment != null ? parentComment.getUser().getId() : null,
76+
req.content()
77+
));
10678

10779
return new CreateCollectionCommentResponse(comment.getId());
10880
}

src/main/java/org/devkor/apu/saerok_server/domain/collection/application/CollectionLikeCommandService.java

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@
22

33
import lombok.RequiredArgsConstructor;
44
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.LikeStatusResponse;
5+
import org.devkor.apu.saerok_server.domain.collection.application.event.CollectionNotificationEvent;
56
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
67
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionLike;
78
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionLikeRepository;
89
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionRepository;
9-
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.ActionKind;
10-
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Actor;
11-
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
12-
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Target;
1310
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
1411
import org.devkor.apu.saerok_server.domain.user.core.repository.UserRepository;
1512
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
1613
import org.devkor.apu.saerok_server.global.shared.exception.NotFoundException;
14+
import org.springframework.context.ApplicationEventPublisher;
1715
import org.springframework.stereotype.Service;
1816
import org.springframework.transaction.annotation.Transactional;
1917

@@ -25,7 +23,7 @@ public class CollectionLikeCommandService {
2523
private final CollectionLikeRepository collectionLikeRepository;
2624
private final CollectionRepository collectionRepository;
2725
private final UserRepository userRepository;
28-
private final NotifyActionDsl notifyAction;
26+
private final ApplicationEventPublisher eventPublisher;
2927

3028
/**
3129
* 좋아요 토글 (추가/제거)
@@ -51,13 +49,12 @@ public LikeStatusResponse toggleLikeResponse(Long userId, Long collectionId) {
5149
UserBirdCollectionLike like = new UserBirdCollectionLike(user, collection);
5250
collectionLikeRepository.save(like);
5351

54-
// 자신의 컬렉션이 아닌 경우에만 푸시 알림 발송
52+
// 알림 전송
5553
if (!collection.getUser().getId().equals(userId)) {
56-
notifyAction
57-
.by(Actor.of(userId, user.getNickname()))
58-
.on(Target.collection(collectionId))
59-
.did(ActionKind.LIKE)
60-
.to(collection.getUser().getId());
54+
eventPublisher.publishEvent(new CollectionNotificationEvent.CollectionLiked(
55+
userId, user.getNickname(),
56+
collectionId, collection.getUser().getId()
57+
));
6158
}
6259

6360
return new LikeStatusResponse(true);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.devkor.apu.saerok_server.domain.collection.application.event;
2+
3+
public sealed interface CollectionNotificationEvent {
4+
5+
record CommentCreated(
6+
Long actorId, String actorNickname,
7+
Long collectionId, Long collectionOwnerId,
8+
Long parentCommentId, Long parentCommentOwnerId,
9+
String commentContent
10+
) implements CollectionNotificationEvent {}
11+
12+
record CollectionLiked(
13+
Long actorId, String actorNickname,
14+
Long collectionId, Long collectionOwnerId
15+
) implements CollectionNotificationEvent {}
16+
17+
record BirdIdSuggested(
18+
Long actorId, String actorNickname,
19+
Long collectionId, Long collectionOwnerId,
20+
String suggestedBirdName
21+
) implements CollectionNotificationEvent {}
22+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.devkor.apu.saerok_server.domain.collection.application.event;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
6+
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.ActionKind;
7+
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Actor;
8+
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Target;
9+
import org.springframework.scheduling.annotation.Async;
10+
import org.springframework.stereotype.Component;
11+
import org.springframework.transaction.event.TransactionPhase;
12+
import org.springframework.transaction.event.TransactionalEventListener;
13+
14+
@Slf4j
15+
@Component
16+
@RequiredArgsConstructor
17+
public class CollectionNotificationWorker {
18+
19+
private final NotifyActionDsl notifyAction;
20+
21+
@Async("pushNotificationExecutor")
22+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
23+
public void handle(CollectionNotificationEvent.CommentCreated event) {
24+
try {
25+
Actor actor = Actor.of(event.actorId(), event.actorNickname());
26+
27+
if (event.parentCommentId() != null) {
28+
// 대댓글: 원댓글 작성자에게 REPLY 알림
29+
if (!event.parentCommentOwnerId().equals(event.actorId())) {
30+
notifyAction
31+
.by(actor)
32+
.on(Target.comment(event.parentCommentId()))
33+
.did(ActionKind.REPLY)
34+
.comment(event.commentContent())
35+
.to(event.parentCommentOwnerId());
36+
}
37+
// 컬렉션 소유자에게 COMMENT 알림 (원댓글 작성자와 다른 경우에만)
38+
if (!event.collectionOwnerId().equals(event.actorId())
39+
&& !event.collectionOwnerId().equals(event.parentCommentOwnerId())) {
40+
notifyAction
41+
.by(actor)
42+
.on(Target.collection(event.collectionId()))
43+
.did(ActionKind.COMMENT)
44+
.comment(event.commentContent())
45+
.to(event.collectionOwnerId());
46+
}
47+
} else {
48+
// 원댓글: 컬렉션 소유자에게 COMMENT 알림
49+
if (!event.collectionOwnerId().equals(event.actorId())) {
50+
notifyAction
51+
.by(actor)
52+
.on(Target.collection(event.collectionId()))
53+
.did(ActionKind.COMMENT)
54+
.comment(event.commentContent())
55+
.to(event.collectionOwnerId());
56+
}
57+
}
58+
} catch (Exception e) {
59+
log.error("Failed to send collection comment notification: collectionId={}, actorId={}",
60+
event.collectionId(), event.actorId(), e);
61+
}
62+
}
63+
64+
@Async("pushNotificationExecutor")
65+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
66+
public void handle(CollectionNotificationEvent.CollectionLiked event) {
67+
try {
68+
notifyAction
69+
.by(Actor.of(event.actorId(), event.actorNickname()))
70+
.on(Target.collection(event.collectionId()))
71+
.did(ActionKind.LIKE)
72+
.to(event.collectionOwnerId());
73+
} catch (Exception e) {
74+
log.error("Failed to send collection like notification: collectionId={}, actorId={}",
75+
event.collectionId(), event.actorId(), e);
76+
}
77+
}
78+
79+
@Async("pushNotificationExecutor")
80+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
81+
public void handle(CollectionNotificationEvent.BirdIdSuggested event) {
82+
try {
83+
notifyAction
84+
.by(Actor.of(event.actorId(), event.actorNickname()))
85+
.on(Target.collection(event.collectionId()))
86+
.did(ActionKind.SUGGEST_BIRD_ID)
87+
.suggestedName(event.suggestedBirdName())
88+
.to(event.collectionOwnerId());
89+
} catch (Exception e) {
90+
log.error("Failed to send bird ID suggestion notification: collectionId={}, actorId={}",
91+
event.collectionId(), event.actorId(), e);
92+
}
93+
}
94+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.devkor.apu.saerok_server.domain.community.api.dto.common;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
import java.time.LocalDateTime;
6+
7+
public record CommunityFreeBoardPostInfo(
8+
@Schema(description = "게시글 ID", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
9+
Long postId,
10+
@Schema(description = "작성자 ID", example = "3", requiredMode = Schema.RequiredMode.REQUIRED)
11+
Long userId,
12+
@Schema(description = "작성자 닉네임", example = "새록마스터", requiredMode = Schema.RequiredMode.REQUIRED)
13+
String nickname,
14+
@Schema(description = "작성자 프로필 이미지 URL", requiredMode = Schema.RequiredMode.REQUIRED)
15+
String profileImageUrl,
16+
@Schema(description = "작성자 썸네일 프로필 이미지 URL (320px 너비)", requiredMode = Schema.RequiredMode.REQUIRED)
17+
String thumbnailProfileImageUrl,
18+
@Schema(description = "게시글 내용", example = "오늘 한강공원에서 백로를 발견했어요!", requiredMode = Schema.RequiredMode.REQUIRED)
19+
String content,
20+
@Schema(description = "작성 시각", example = "2025-07-05T03:10:00", requiredMode = Schema.RequiredMode.REQUIRED)
21+
LocalDateTime createdAt,
22+
@Schema(description = "최종 수정 시각", example = "2025-07-05T04:20:00", requiredMode = Schema.RequiredMode.REQUIRED)
23+
LocalDateTime updatedAt
24+
) {
25+
}

src/main/java/org/devkor/apu/saerok_server/domain/community/api/dto/response/GetCommunityMainResponse.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import io.swagger.v3.oas.annotations.media.Schema;
44
import org.devkor.apu.saerok_server.domain.community.api.dto.common.CommunityCollectionInfo;
5-
import org.devkor.apu.saerok_server.domain.freeboard.api.dto.response.FreeBoardPostPreviewResponse;
5+
import org.devkor.apu.saerok_server.domain.community.api.dto.common.CommunityFreeBoardPostInfo;
66

77
import java.util.List;
88

@@ -17,6 +17,6 @@ public record GetCommunityMainResponse(
1717
@Schema(description = "동정 요청 새록 목록 (최대 3개)")
1818
List<CommunityCollectionInfo> pendingCollections,
1919

20-
@Schema(description = "최근 자유게시판 글 (최대 3개)")
21-
List<FreeBoardPostPreviewResponse> recentFreeBoardPosts
20+
@Schema(description = "최근 자유게시판 글 (최대 5개)")
21+
List<CommunityFreeBoardPostInfo> recentFreeBoardPosts
2222
) {}

0 commit comments

Comments
 (0)