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
@@ -0,0 +1,8 @@
package team.wego.wegobackend.group.v2.application.event;

public record GroupCreatedEvent(
Long groupId,
Long hostUserId
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package team.wego.wegobackend.group.v2.application.event;

import java.util.List;

public record GroupDeletedEvent(
// deleteHard()에서 삭제 전에 group/host 정보를 꺼내서 이벤트에 실어 보내면,
//AFTER_COMMIT에서도 DB 재조회가 필요 없어진다.
Long groupId,
Long hostId,
String hostNickName,
String groupTitle,
List<Long> attendeeUserIds
) {

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package team.wego.wegobackend.group.v2.application.event;

public record GroupJoinApprovedEvent(
Long groupId,
Long approverUserId,
Long targetUserId
) {

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package team.wego.wegobackend.group.v2.application.event;

public record GroupJoinKickedEvent(
Long groupId,
Long hostId,
Long targetUserId
) {

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package team.wego.wegobackend.group.v2.application.event;

public record GroupJoinRejectedEvent(
Long groupId,
Long approverUserId,
Long targetUserId
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package team.wego.wegobackend.group.v2.application.event;

public record GroupJoinRequestedEvent(
Long groupId,
Long hostUserId,
Long requesterUserId
) {

}



Comment on lines +10 to +12
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Unnecessary blank lines should be removed. There are extra blank lines at the end of the file that should be cleaned up for consistency.

Suggested change

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package team.wego.wegobackend.group.v2.application.event;

public record GroupJoinedEvent(
Long groupId,
Long hostId,
Long joinerUserId
) {

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package team.wego.wegobackend.group.v2.application.event;

public record GroupLeftEvent(
Long groupId,
Long hostId,
Long leaverUserId
) {

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package team.wego.wegobackend.group.v2.application.event;

import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import team.wego.wegobackend.group.v2.domain.entity.GroupV2;
import team.wego.wegobackend.notification.application.dto.NotificationType;
import team.wego.wegobackend.notification.domain.Notification;
import team.wego.wegobackend.user.domain.User;

@Getter(AccessLevel.PUBLIC)
@Builder(access = AccessLevel.PUBLIC)
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Default toString(): GroupInfo inherits toString() from Object, and so is not suitable for printing.
Default toString(): ActorUser inherits toString() from Object, and so is not suitable for printing.

Copilot uses AI. Check for mistakes.
public class NotificationEvent {

private Long id; // notificationId
private String message;
private NotificationType type;

private LocalDateTime createdAt;
private LocalDateTime readAt; // null == unread

private ActorUser user; // 모임 생성자
private GroupInfo group; // 모임

public boolean isRead() {
return readAt != null;
}

public static NotificationEvent of(Notification n, User actor, GroupV2 group) {
return NotificationEvent.builder()
.id(n.getId())
.message(n.getMessage())
.type(n.getType())
.createdAt(n.getCreatedAt())
.readAt(n.getReadAt())
.user(ActorUser.from(actor))
.group(group != null ? GroupInfo.from(group) : null)
.build();
}

@Getter(AccessLevel.PUBLIC)
@Builder(access = AccessLevel.PUBLIC)
public static class ActorUser {
private Long id;
private String nickName;
private String profileImage;

public static ActorUser from(User u) {
if (u == null) return null;
return ActorUser.builder()
.id(u.getId())
.nickName(u.getNickName())
.profileImage(u.getProfileImage())
.build();
}
}

@Getter(AccessLevel.PUBLIC)
@Builder(access = AccessLevel.PUBLIC)
public static class GroupInfo {
private Long id;
private String title;

public static GroupInfo from(GroupV2 g) {
if (g == null) return null;
return GroupInfo.builder()
.id(g.getId())
.title(g.getTitle())
.build();
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package team.wego.wegobackend.group.v2.application.handler;


import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import team.wego.wegobackend.group.v2.domain.entity.GroupV2;
import team.wego.wegobackend.group.v2.domain.repository.GroupV2Repository;
import team.wego.wegobackend.notification.application.dispatcher.NotificationDispatcher;
import team.wego.wegobackend.notification.domain.Notification;
import team.wego.wegobackend.user.domain.User;
import team.wego.wegobackend.user.repository.FollowRepository;
import team.wego.wegobackend.user.repository.UserRepository;
import team.wego.wegobackend.user.repository.query.FollowerNotifyRow;

@Slf4j
@Service
@RequiredArgsConstructor
public class GroupCreateNotificationHandler {

private final FollowRepository followRepository;
private final UserRepository userRepository;
private final GroupV2Repository groupV2Repository;

private final NotificationDispatcher notificationDispatcher;

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle(Long groupId, Long hostUserId) {
log.info("[NOTI] start handle. groupId={}, hostId={}", groupId, hostUserId);

List<FollowerNotifyRow> test = followRepository.findFollowersForNotify(hostUserId, null, 10);
log.info("[NOTI] follower rows size={}", test.size());
Comment on lines +35 to +36
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

디버그/테스트 코드 제거 필요

Lines 35-36은 10개의 팔로워를 조회하여 로깅하는 테스트용 코드로 보입니다. 실제 배치 처리 로직(Lines 47-74)과 중복되며 프로덕션 코드에는 불필요합니다.

🔎 수정 제안
     @Transactional(propagation = Propagation.REQUIRES_NEW)
     public void handle(Long groupId, Long hostUserId) {
         log.info("[NOTI] start handle. groupId={}, hostId={}", groupId, hostUserId);
 
-        List<FollowerNotifyRow> test = followRepository.findFollowersForNotify(hostUserId, null, 10);
-        log.info("[NOTI] follower rows size={}", test.size());
-
         User host = userRepository.findById(hostUserId)
                 .orElseThrow();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
List<FollowerNotifyRow> test = followRepository.findFollowersForNotify(hostUserId, null, 10);
log.info("[NOTI] follower rows size={}", test.size());
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle(Long groupId, Long hostUserId) {
log.info("[NOTI] start handle. groupId={}, hostId={}", groupId, hostUserId);
User host = userRepository.findById(hostUserId)
.orElseThrow();
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/group/v2/application/handler/GroupCreateNotificationHandler.java
around lines 35-36, remove the debug/test code that calls
followRepository.findFollowersForNotify(hostUserId, null, 10) and the
corresponding log line; this duplicates the real batch processing later (lines
47-74) and is unnecessary in production. Delete those two lines so only the
intended batch notification logic remains, and run tests to ensure no
compilation or unused-import issues remain.

Comment on lines +35 to +36
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Debug code should be removed before merging. These test log statements appear to be leftover from development and should be cleaned up.

Copilot uses AI. Check for mistakes.

User host = userRepository.findById(hostUserId)
.orElseThrow();

GroupV2 group = groupV2Repository.findById(groupId)
.orElseThrow();

Long cursor = null;
final int size = 500;

while (true) {
List<FollowerNotifyRow> rows =
followRepository.findFollowersForNotify(
hostUserId, cursor, size
);

if (rows.isEmpty()) {
break;
}

List<Notification> notifications = new ArrayList<>(rows.size());

for (FollowerNotifyRow row : rows) {
User receiver = userRepository.getReferenceById(row.userId());
notifications.add(
Notification.createGroupCreateNotification(
receiver,
host,
group
)
);
Comment on lines +59 to +67
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Potential N+1 query problem. For each FollowerNotifyRow in the batch, userRepository.getReferenceById is called individually to create User references. Consider using a batch query approach like userRepository.findAllById to fetch all users at once per batch, which would be more efficient.

Copilot uses AI. Check for mistakes.
}

// 공통 디스패처 호출
notificationDispatcher.dispatch(notifications, host, group);

cursor = rows.getLast().followId();
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package team.wego.wegobackend.group.v2.application.handler;

import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import team.wego.wegobackend.group.domain.exception.GroupErrorCode;
import team.wego.wegobackend.group.domain.exception.GroupException;
import team.wego.wegobackend.group.v2.application.event.GroupDeletedEvent;
import team.wego.wegobackend.notification.application.dispatcher.NotificationDispatcher;
import team.wego.wegobackend.notification.domain.Notification;
import team.wego.wegobackend.user.domain.User;
import team.wego.wegobackend.user.repository.UserRepository;

@Slf4j
@Service
@RequiredArgsConstructor
public class GroupDeleteNotificationHandler {

private final UserRepository userRepository;
private final NotificationDispatcher notificationDispatcher;

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle(GroupDeletedEvent event) {
List<Long> ids = event.attendeeUserIds();
int size = (ids == null ? 0 : ids.size());

log.info("[GROUP_DELETE][HANDLER] start groupId={} hostId={} attendeeCount={}",
event.groupId(), event.hostId(), size);

if (ids == null || ids.isEmpty()) {
return;
}

User host = userRepository.findById(event.hostId())
.orElseThrow(() -> new GroupException(GroupErrorCode.HOST_USER_NOT_FOUND,
event.hostId()));

List<Notification> notifications = new ArrayList<>(ids.size());
for (Long receiverId : ids) {
if (receiverId.equals(event.hostId())) {
continue;
}
User receiver = userRepository.getReferenceById(receiverId);
notifications.add(
Notification.createGroupDeleteNotification(receiver, host, event.groupId(),
event.groupTitle()));
}

log.info("[GROUP_DELETE][HANDLER] built notifications size={}", notifications.size());
notificationDispatcher.dispatch(notifications, host, null);

log.info("[GROUP_DELETE][HANDLER] done groupId={}", event.groupId());
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package team.wego.wegobackend.group.v2.application.handler;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import team.wego.wegobackend.group.domain.exception.GroupErrorCode;
import team.wego.wegobackend.group.domain.exception.GroupException;
import team.wego.wegobackend.group.v2.application.event.NotificationEvent;
import team.wego.wegobackend.group.v2.domain.entity.GroupV2;
import team.wego.wegobackend.group.v2.domain.repository.GroupV2Repository;
import team.wego.wegobackend.notification.domain.Notification;
import team.wego.wegobackend.notification.repository.NotificationRepository;
import team.wego.wegobackend.notification.application.SseEmitterService;
import team.wego.wegobackend.user.domain.User;
import team.wego.wegobackend.user.exception.UserNotFoundException;
import team.wego.wegobackend.user.repository.UserRepository;

@Service
@RequiredArgsConstructor
public class GroupJoinDecisionNotificationHandler {

private final UserRepository userRepository;
private final GroupV2Repository groupV2Repository;

private final NotificationRepository notificationRepository;
private final SseEmitterService sseEmitterService;

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleApproved(Long groupId, Long approverUserId, Long targetUserId) {
handle(groupId, approverUserId, targetUserId, true);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleRejected(Long groupId, Long approverUserId, Long targetUserId) {
handle(groupId, approverUserId, targetUserId, false);
}

private void handle(Long groupId, Long approverUserId, Long targetUserId, boolean approved) {
User actor = userRepository.findById(approverUserId)
.orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, approverUserId));

User receiver = userRepository.findById(targetUserId)
.orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, targetUserId));

GroupV2 group = groupV2Repository.findById(groupId)
.orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId));

Notification notification = approved
? Notification.createGroupJoinApprovedNotification(receiver, actor, group)
: Notification.createGroupJoinRejectedNotification(receiver, actor, group);

Notification saved = notificationRepository.save(notification);

NotificationEvent dto = NotificationEvent.of(saved, actor, group);
sseEmitterService.sendNotification(receiver.getId(), dto);
}
}
Loading