Skip to content
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
@@ -1,23 +1,21 @@
package org.devkor.apu.saerok_server.domain.collection.application;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.*;
import org.devkor.apu.saerok_server.domain.collection.application.event.CollectionNotificationEvent;
import org.devkor.apu.saerok_server.domain.collection.core.entity.*;
import org.devkor.apu.saerok_server.domain.collection.core.repository.*;
import org.devkor.apu.saerok_server.domain.dex.bird.core.entity.Bird;
import org.devkor.apu.saerok_server.domain.dex.bird.core.repository.BirdRepository;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.ActionKind;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Actor;
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Target;
import org.devkor.apu.saerok_server.domain.admin.stat.application.BirdIdRequestHistoryRecorder;
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
import org.devkor.apu.saerok_server.domain.user.core.repository.UserRepository;
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
import org.devkor.apu.saerok_server.global.shared.exception.ForbiddenException;
import org.devkor.apu.saerok_server.global.shared.exception.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.OffsetDateTime;

Expand All @@ -30,8 +28,8 @@ public class BirdIdSuggestionCommandService {
private final CollectionRepository collectionRepo;
private final BirdRepository birdRepo;
private final UserRepository userRepo;
private final NotifyActionDsl notifyAction;
private final BirdIdRequestHistoryRecorder birdReqHistory;
private final ApplicationEventPublisher eventPublisher;

public SuggestBirdIdResponse suggest(Long userId, Long collectionId, Long birdId) {
User user = userRepo.findById(userId)
Expand Down Expand Up @@ -88,12 +86,11 @@ public SuggestBirdIdResponse suggest(Long userId, Long collectionId, Long birdId
}
// 최초 제안인 경우에만 알림 발송
if (!birdAlreadySuggested) {
notifyAction
.by(Actor.of(userId, user.getNickname()))
.on(Target.collection(collectionId))
.did(ActionKind.SUGGEST_BIRD_ID)
.suggestedName(bird.getName().getKoreanName())
.to(collection.getUser().getId());
eventPublisher.publishEvent(new CollectionNotificationEvent.BirdIdSuggested(
userId, user.getNickname(),
collectionId, collection.getUser().getId(),
bird.getName().getKoreanName()
));
}

return new SuggestBirdIdResponse(suggestionId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.devkor.apu.saerok_server.domain.collection.application;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.UpdateCollectionResponse;
import org.devkor.apu.saerok_server.domain.collection.application.dto.CreateCollectionCommand;
Expand All @@ -26,6 +25,7 @@
import org.devkor.apu.saerok_server.global.shared.infra.ImageService;
import org.locationtech.jts.geom.Point;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.OffsetDateTime;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
package org.devkor.apu.saerok_server.domain.collection.application;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.devkor.apu.saerok_server.domain.collection.api.dto.request.CreateCollectionCommentRequest;
import org.devkor.apu.saerok_server.domain.collection.api.dto.request.UpdateCollectionCommentRequest;
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.CreateCollectionCommentResponse;
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.UpdateCollectionCommentResponse;
import org.devkor.apu.saerok_server.domain.collection.application.event.CollectionNotificationEvent;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionComment;
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionCommentRepository;
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionRepository;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.ActionKind;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Actor;
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Target;
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
import org.devkor.apu.saerok_server.domain.user.core.repository.UserRepository;
import org.devkor.apu.saerok_server.global.shared.exception.ForbiddenException;
import org.devkor.apu.saerok_server.global.shared.exception.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
Expand All @@ -28,7 +26,7 @@ public class CollectionCommentCommandService {
private final CollectionCommentRepository commentRepository;
private final CollectionRepository collectionRepository;
private final UserRepository userRepository;
private final NotifyActionDsl notifyAction;
private final ApplicationEventPublisher eventPublisher;

/* 댓글 작성 */
public CreateCollectionCommentResponse createComment(Long userId,
Expand Down Expand Up @@ -70,39 +68,13 @@ public CreateCollectionCommentResponse createComment(Long userId,
commentRepository.save(comment);

// 알림 전송
if (parentComment != null) {
// 대댓글인 경우
// 1) 원댓글 작성자에게 REPLY 알림
if (!parentComment.getUser().getId().equals(userId)) {
notifyAction
.by(Actor.of(userId, user.getNickname()))
.on(Target.comment(parentComment.getId()))
.did(ActionKind.REPLY)
.comment(req.content())
.to(parentComment.getUser().getId());
}

// 2) 컬렉션 소유자에게 COMMENT 알림 (원댓글 작성자와 다른 경우에만)
if (!collection.getUser().getId().equals(userId)
&& !collection.getUser().getId().equals(parentComment.getUser().getId())) {
notifyAction
.by(Actor.of(userId, user.getNickname()))
.on(Target.collection(collectionId))
.did(ActionKind.COMMENT)
.comment(req.content())
.to(collection.getUser().getId());
}
} else {
// 원댓글인 경우
if (!collection.getUser().getId().equals(userId)) {
notifyAction
.by(Actor.of(userId, user.getNickname()))
.on(Target.collection(collectionId))
.did(ActionKind.COMMENT)
.comment(req.content())
.to(collection.getUser().getId());
}
}
eventPublisher.publishEvent(new CollectionNotificationEvent.CommentCreated(
userId, user.getNickname(),
collectionId, collection.getUser().getId(),
parentComment != null ? parentComment.getId() : null,
parentComment != null ? parentComment.getUser().getId() : null,
req.content()
));

return new CreateCollectionCommentResponse(comment.getId());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

import lombok.RequiredArgsConstructor;
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.LikeStatusResponse;
import org.devkor.apu.saerok_server.domain.collection.application.event.CollectionNotificationEvent;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionLike;
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionLikeRepository;
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionRepository;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.ActionKind;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Actor;
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Target;
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
import org.devkor.apu.saerok_server.domain.user.core.repository.UserRepository;
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
import org.devkor.apu.saerok_server.global.shared.exception.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -25,7 +23,7 @@ public class CollectionLikeCommandService {
private final CollectionLikeRepository collectionLikeRepository;
private final CollectionRepository collectionRepository;
private final UserRepository userRepository;
private final NotifyActionDsl notifyAction;
private final ApplicationEventPublisher eventPublisher;

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

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

return new LikeStatusResponse(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.devkor.apu.saerok_server.domain.collection.application.event;

public sealed interface CollectionNotificationEvent {

record CommentCreated(
Long actorId, String actorNickname,
Long collectionId, Long collectionOwnerId,
Long parentCommentId, Long parentCommentOwnerId,
String commentContent
) implements CollectionNotificationEvent {}

record CollectionLiked(
Long actorId, String actorNickname,
Long collectionId, Long collectionOwnerId
) implements CollectionNotificationEvent {}

record BirdIdSuggested(
Long actorId, String actorNickname,
Long collectionId, Long collectionOwnerId,
String suggestedBirdName
) implements CollectionNotificationEvent {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.devkor.apu.saerok_server.domain.collection.application.event;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.ActionKind;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Actor;
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Target;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Slf4j
@Component
@RequiredArgsConstructor
public class CollectionNotificationWorker {

private final NotifyActionDsl notifyAction;

@Async("pushNotificationExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(CollectionNotificationEvent.CommentCreated event) {
try {
Actor actor = Actor.of(event.actorId(), event.actorNickname());

if (event.parentCommentId() != null) {
// 대댓글: 원댓글 작성자에게 REPLY 알림
if (!event.parentCommentOwnerId().equals(event.actorId())) {
notifyAction
.by(actor)
.on(Target.comment(event.parentCommentId()))
.did(ActionKind.REPLY)
.comment(event.commentContent())
.to(event.parentCommentOwnerId());
}
// 컬렉션 소유자에게 COMMENT 알림 (원댓글 작성자와 다른 경우에만)
if (!event.collectionOwnerId().equals(event.actorId())
&& !event.collectionOwnerId().equals(event.parentCommentOwnerId())) {
notifyAction
.by(actor)
.on(Target.collection(event.collectionId()))
.did(ActionKind.COMMENT)
.comment(event.commentContent())
.to(event.collectionOwnerId());
}
} else {
// 원댓글: 컬렉션 소유자에게 COMMENT 알림
if (!event.collectionOwnerId().equals(event.actorId())) {
notifyAction
.by(actor)
.on(Target.collection(event.collectionId()))
.did(ActionKind.COMMENT)
.comment(event.commentContent())
.to(event.collectionOwnerId());
}
}
} catch (Exception e) {
log.error("Failed to send collection comment notification: collectionId={}, actorId={}",
event.collectionId(), event.actorId(), e);
}
}

@Async("pushNotificationExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(CollectionNotificationEvent.CollectionLiked event) {
try {
notifyAction
.by(Actor.of(event.actorId(), event.actorNickname()))
.on(Target.collection(event.collectionId()))
.did(ActionKind.LIKE)
.to(event.collectionOwnerId());
} catch (Exception e) {
log.error("Failed to send collection like notification: collectionId={}, actorId={}",
event.collectionId(), event.actorId(), e);
}
}

@Async("pushNotificationExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(CollectionNotificationEvent.BirdIdSuggested event) {
try {
notifyAction
.by(Actor.of(event.actorId(), event.actorNickname()))
.on(Target.collection(event.collectionId()))
.did(ActionKind.SUGGEST_BIRD_ID)
.suggestedName(event.suggestedBirdName())
.to(event.collectionOwnerId());
} catch (Exception e) {
log.error("Failed to send bird ID suggestion notification: collectionId={}, actorId={}",
event.collectionId(), event.actorId(), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.devkor.apu.saerok_server.domain.community.api.dto.common;

import io.swagger.v3.oas.annotations.media.Schema;

import java.time.LocalDateTime;

public record CommunityFreeBoardPostInfo(
@Schema(description = "게시글 ID", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
Long postId,
@Schema(description = "작성자 ID", example = "3", requiredMode = Schema.RequiredMode.REQUIRED)
Long userId,
@Schema(description = "작성자 닉네임", example = "새록마스터", requiredMode = Schema.RequiredMode.REQUIRED)
String nickname,
@Schema(description = "작성자 프로필 이미지 URL", requiredMode = Schema.RequiredMode.REQUIRED)
String profileImageUrl,
@Schema(description = "작성자 썸네일 프로필 이미지 URL (320px 너비)", requiredMode = Schema.RequiredMode.REQUIRED)
String thumbnailProfileImageUrl,
@Schema(description = "게시글 내용", example = "오늘 한강공원에서 백로를 발견했어요!", requiredMode = Schema.RequiredMode.REQUIRED)
String content,
@Schema(description = "작성 시각", example = "2025-07-05T03:10:00", requiredMode = Schema.RequiredMode.REQUIRED)
LocalDateTime createdAt,
@Schema(description = "최종 수정 시각", example = "2025-07-05T04:20:00", requiredMode = Schema.RequiredMode.REQUIRED)
LocalDateTime updatedAt
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

import java.util.List;

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

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