Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
89e7af6
feat: 미팅 취소 도메인 구현
Sehi55 Jul 16, 2026
ef6d494
feat: 미팅 전체 취소 투표 기능 구현
Sehi55 Jul 21, 2026
f95bc2c
refactor: 안쓰는 import 정리
Sehi55 Jul 21, 2026
83a2a9e
fix: 취소 투표 중 미팅방 참여자 변경 차단
Sehi55 Jul 21, 2026
fc3ac92
fix: 매칭방 퇴장 시 빠른 입장 상태로 전환
Sehi55 Jul 21, 2026
424f13f
feat: 빠른 입장 사용자를 매칭 채팅방에 연결
Sehi55 Jul 21, 2026
88bfa11
feat: 매칭 성사 후 동성 채팅방 비활성화
Sehi55 Jul 21, 2026
6b02449
feat: 코드 빠른 입장 사용자를 매칭 채팅방에 연결
Sehi55 Jul 21, 2026
3a1afa5
fix: 매칭 상태에서 남녀 채팅방 ID 반환
Sehi55 Jul 21, 2026
2dfcd21
feat: 전체 미팅 취소를 매칭 단위로 전환
Sehi55 Jul 21, 2026
13c910d
fix: 비활성 채팅방 웹소켓 구독 차단
Sehi55 Jul 21, 2026
3a90afa
fix: 미팅 취소 상태 제약 조건 동기화
Sehi55 Jul 21, 2026
28521bc
fix: 매칭 채팅방 재입장 허용
Sehi55 Jul 21, 2026
fa62312
fix: 미팅 취소 만료 처리를 건별 트랜잭션으로 분리
Sehi55 Jul 21, 2026
75d4ac1
feat: 채팅 시스템 메시지 기록 및 전송
Sehi55 Jul 21, 2026
e82c92c
merge: 미팅 취소 투표 기능 통합
Sehi55 Jul 21, 2026
b7465e9
refactor: 시스템 알림을 웹소켓과 푸시로 통합
Sehi55 Jul 22, 2026
a998d3f
feat: 시스템 메시지 제목과 동적 정보 저장
Sehi55 Jul 22, 2026
64528af
feat: 미팅 검증 인원 추가 및 데이터 저장 로직 추가
Sehi55 Jul 22, 2026
1cf3a7c
fix: 만남인증 참여 인원 중복 집계 방지
Sehi55 Jul 22, 2026
d5d7a51
fix: 활성 채팅 멤버 중복 데이터 정리
Sehi55 Aug 6, 2026
749a9ce
fix: 양 팀 매칭 타임아웃 처리
Sehi55 Aug 6, 2026
e7769d4
fix: 실패한 매칭 결과 응답 정리
Sehi55 Aug 6, 2026
99a0377
refactor: 안쓰는 import 삭제
Sehi55 Aug 6, 2026
e97149c
feat: api 문서 자동 생성 의존성 추가
Sehi55 Aug 6, 2026
8aeeb3e
fix: 미팅 취소 거절 투표 집계
Sehi55 Aug 6, 2026
c3917fb
fix: 미팅 취소 만료 이벤트 정보 통일
Sehi55 Aug 6, 2026
102cfa4
fix: 만료된 미팅 취소 투표 롤백 방지
Sehi55 Aug 6, 2026
39ac68e
refactor: 미팅 취소 만료 처리 통합
Sehi55 Aug 6, 2026
448f241
fix: 만남 인증 완료 메시지 정보 수정
Sehi55 Aug 6, 2026
8c9a16c
fix: Push 발송을 트랜잭션 커밋 이후로 분리
Sehi55 Aug 6, 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
3 changes: 3 additions & 0 deletions manabom/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ dependencies {
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'

//api 문서
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.17'


}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package mannabom_server.manabom.application.chat.dto.event;

import mannabom_server.manabom.application.chat.message.SystemMessageType;

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

public record ChatSystemMessageEvent(
Long roomId,
SystemMessageType type,
Long actorUserId,
List<Long> recipientUserIds,
Map<String, Object> data,
boolean pushEnabled
) {
public ChatSystemMessageEvent {
recipientUserIds = recipientUserIds == null ? null : List.copyOf(recipientUserIds);
data = data == null ? Map.of() : Map.copyOf(data);
}

public static ChatSystemMessageEvent of(
Long roomId,
SystemMessageType type,
Long actorUserId,
List<Long> recipientUserIds,
Map<String, Object> data
) {
return new ChatSystemMessageEvent(roomId, type, actorUserId, recipientUserIds, data, true);
}

public static ChatSystemMessageEvent withoutPush(
Long roomId,
SystemMessageType type,
Long actorUserId,
List<Long> recipientUserIds,
Map<String, Object> data
) {
return new ChatSystemMessageEvent(roomId, type, actorUserId, recipientUserIds, data, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import lombok.Getter;

import java.time.Instant;
import java.util.List;
import java.util.Map;

//메세지 보내진 사실을 모든 참여자들에게 보내는 이벤트 서버->클
@Getter
Expand All @@ -12,8 +14,14 @@ public class ChatMessageEvent {
private Long roomId;

private Long senderUserId;
private Long actorUserId;
private List<Long> recipientUserIds;
private String messageType;
private String systemEventType;
private String systemTitle;
private String actorNickname;
private String content;
private Map<String, Object> data;

private Long messageId;
private String clientMessageId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
import mannabom_server.manabom.domain.chat.entity.ChatMessage;

import java.time.Instant;
import java.util.Map;

@Getter
@Builder
public class ChatMessageResponse {
private Long messageId;
private String content;
private String messageType;
private String systemEventType;
private String systemTitle;
private Long actorUserId;
private String actorNickname;
private Map<String, Object> data;
private Instant createdAt;

private Long senderId;
Expand All @@ -21,8 +27,13 @@ public static ChatMessageResponse of(ChatMessage msg){
.messageId(msg.getId())
.createdAt(msg.getCreatedAt())
.messageType(msg.getType().name())
.senderId(msg.getUser().getUserId())
.senderId(msg.getUser() != null ? msg.getUser().getUserId() : null)
.content(msg.getContent())
.systemEventType(msg.getSystemEventType())
.systemTitle(msg.getSystemTitle())
.actorUserId(msg.getActorUserId())
.actorNickname(msg.getActorNickname())
.data(msg.getSystemData())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import lombok.Getter;
import mannabom_server.manabom.domain.chat.entity.ChatMessage;
import mannabom_server.manabom.domain.chat.entity.ChatRoom;
import mannabom_server.manabom.domain.chat.enums.ChatRoomType;
import mannabom_server.manabom.domain.chat.enums.ChatMessageType;

import java.time.Instant;

Expand All @@ -21,10 +21,17 @@ public static ChatRoomListResponse of(ChatRoom room,String roomName, ChatMessage
return ChatRoomListResponse.builder()
.roomId(room.getId())
.roomName(roomName)
.lastMessage(lastMsg!=null ? lastMsg.getType().getDisplayMessage(lastMsg.getContent()): null )
.lastMessage(lastMsg != null ? lastMessagePreview(lastMsg) : null)
.lastmessageAt(lastMsg != null ? lastMsg.getCreatedAt() : null)
.hasUnreadMessages(hasUnreadMessages)
.build();
}

private static String lastMessagePreview(ChatMessage message) {
if (message.getType() == ChatMessageType.SYSTEM && message.getSystemTitle() != null) {
return message.getSystemTitle();
}
return message.getType().getDisplayMessage(message.getContent());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package mannabom_server.manabom.application.chat.handler;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import mannabom_server.manabom.application.chat.dto.event.ChatSystemMessageEvent;
import mannabom_server.manabom.application.chat.service.SystemMessageService;
import mannabom_server.manabom.application.notification.dto.MatchFailureEvent;
import mannabom_server.manabom.application.notification.dto.MatchFoundEvent;
import mannabom_server.manabom.application.notification.dto.MatchSuccessEvent;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@RequiredArgsConstructor
@Slf4j
public class ChatSystemMessageEventHandler {

private final SystemMessageService systemMessageService;

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleMatchFound(MatchFoundEvent event) {
try {
systemMessageService.dispatch(
systemMessageService.recordMatchFound(event.matchId(), event.decisionDeadline())
);
} catch (RuntimeException e) {
log.error("매칭 발견 시스템 메시지 처리 실패: matchId={}", event.matchId(), e);
}
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleMatchFailure(MatchFailureEvent event) {
try {
systemMessageService.dispatch(systemMessageService.recordMatchFailure(
event.matchId(),
event.triggerUserId(),
Boolean.TRUE.equals(event.isByTimeout())
));
} catch (RuntimeException e) {
log.error("매칭 실패 시스템 메시지 처리 실패: matchId={}", event.matchId(), e);
}
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleMatchSuccess(MatchSuccessEvent event) {
try {
systemMessageService.dispatch(systemMessageService.recordMatchSuccess(
event.matchId(),
event.triggerUserId(),
event.chatRoomId()
));
} catch (RuntimeException e) {
log.error("매칭 성사 시스템 메시지 처리 실패: matchId={}", event.matchId(), e);
}
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleRoomSystemMessage(ChatSystemMessageEvent event) {
try {
systemMessageService.dispatch(systemMessageService.recordForRoom(event));
} catch (RuntimeException e) {
log.error("채팅방 시스템 메시지 처리 실패: roomId={}, type={}",
event.roomId(), event.type(), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package mannabom_server.manabom.application.chat.message;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;

import java.util.Map;

@Getter
@RequiredArgsConstructor
public enum SystemMessageType {
MATCHING_STARTED(
"매칭을 시작했어요",
"팀장님이 매칭을 시작하셨어요."
),
MATCHING_STOPPED(
"매칭을 중단했어요",
"팀장님이 매칭을 중단하셨어요."
),
MATCH_FOUND(
"미팅할 상대 팀을 찾았어요‼️✨",
"수락 가능 시간 안에 팀원들과 상의해 주세요.\n"
+ "팀장은 상단 헤더에서 수락을 확정해 주세요.\n"
+ "수락 가능 시간이 지나면 자동 거절돼요."
),
MATCH_REJECTED_BY_LEADER(
"아쉽네요",
"팀장님이 매칭을 거절했어요.\n새로운 상대 팀을 찾고 있어요."
),
MATCH_REJECTED_BY_OPPONENT(
"아쉽네요",
"이번에는 상대 팀과 연결되지 않았어요.\n다시 매칭 신청을 눌러주세요!"
),
MATCH_TIMED_OUT(
"응답 시간이 지났어요",
"수락 가능 시간이 지나 매칭이 자동으로 거절됐어요."
),
OPPONENT_MATCH_TIMED_OUT(
"아쉽네요",
"상대 팀의 응답 시간이 지나 매칭이 취소됐어요.\n다시 매칭 신청을 눌러주세요!"
),
MATCH_COMPLETED(
"미팅이 성사되었어요 🎉🎉",
"이 채팅방은 더 이상 이용하실 수 없습니다.\n"
+ "새로운 채팅방으로 이동하여 상대 팀과 이야기를 나눠보세요!"
),
CHAT_ROOM_CREATED(
"매칭이 성사되었어요 🎉",
"서로 인사를 나누고 즐거운 대화를 시작해 보세요."
),
MEETING_VERIFICATION_STARTED(
"만남인증 시작",
"만남인증이 시작되었어요."
),
MEETING_VERIFICATION_SUCCEEDED(
"만남인증 성공",
"만남인증이 완료되었어요."
),
MEETING_VERIFICATION_FAILED(
"만남인증 실패",
"만남인증이 완료되지 않았어요."
),
MEETING_CANCELLATION_VOTE_STARTED(
"미팅 취소 투표",
"미팅 취소 투표가 시작되었어요.\n"
+ "모든 인원이 취소에 동의하면 채팅방이 사라지고 채팅방에서 사용된 팅은 위약금으로 제외하고 환불해드려요."
),
MEETING_CANCELLATION_APPROVED(
"미팅 취소 투표 결과",
"모두가 동의하여 미팅과 채팅방이 취소되었어요."
),
MEETING_CANCELLATION_REJECTED(
"미팅 취소 투표 결과",
"모두가 동의하지 않아 취소되지 않았어요."
),
MEETING_CANCELLATION_EXPIRED(
"미팅 취소 투표 결과",
"투표 시간이 지나 미팅이 취소되지 않았어요."
),
PHOTO_REQUESTED(
"프로필 공개 요청",
"수락할 시 서로의 프로필이 공개됩니다.\n헤더를 내려 수락하기 버튼을 눌러주세요."
),
PHOTO_REQUEST_ACCEPTED(
"서로의 프로필이 공개되었습니다! ✨",
"서로의 프로필을 눌러 더 다양한 대화를 나눠보세요!"
),
PHOTO_REQUEST_REJECTED(
"프로필 공개가 거절되었어요",
"아직 상대가 조심스러운가 봐요.\n더 대화를 나눠보세요!"
);

private final String defaultTitle;
private final String defaultBody;

public RenderedSystemMessage render(String actorNickname, Map<String, Object> data) {
String nickname = StringUtils.hasText(actorNickname) ? actorNickname : "사용자";
int participantCount = number(data, "participantCount");
int verifiedParticipantCount = data != null && data.containsKey("verifiedParticipantCount")
? number(data, "verifiedParticipantCount")
: participantCount;

return switch (this) {
case MATCHING_STARTED -> message(
defaultTitle,
"팀장 " + nickname + "님이 매칭을 시작하셨어요."
);
case MATCHING_STOPPED -> message(
defaultTitle,
"팀장 " + nickname + "님이 매칭을 중단하셨어요."
);
case MATCH_REJECTED_BY_LEADER -> message(
defaultTitle,
"팀장 " + nickname + "님이 매칭을 거절했어요.\n새로운 상대 팀을 찾고 있어요."
);
case MEETING_VERIFICATION_STARTED -> message(
defaultTitle,
nickname + "님이 만남인증을 시작했어요."
);
case MEETING_VERIFICATION_SUCCEEDED -> message(
defaultTitle,
nickname + "님의 참여로 만남인증이 완료되었어요.\n"
+ "참여인원 : " + verifiedParticipantCount + "명"
);
case MEETING_VERIFICATION_FAILED -> message(
defaultTitle,
nickname + "님이 시작한 만남인증이 완료되지 않았어요.\n"
+ "참여인원 : " + participantCount + "명"
);
case MEETING_CANCELLATION_VOTE_STARTED -> message(
defaultTitle,
nickname + "님이 미팅 취소 투표를 시작하셨어요!\n"
+ "모든 인원이 취소에 동의하면 채팅방이 사라지고 채팅방에서 사용된 팅은 위약금으로 제외하고 환불해드려요."
);
case PHOTO_REQUESTED -> message(
nickname + "님이 프로필 공개를 요청했어요",
defaultBody
);
case PHOTO_REQUEST_ACCEPTED -> message(
defaultTitle,
nickname + "님이 프로필 공개 요청을 수락했어요.\n" + defaultBody
);
case PHOTO_REQUEST_REJECTED -> message(
defaultTitle,
nickname + "님이 아직은 조심스러운가 봐요.\n더 대화를 나눠보세요!"
);
default -> message(defaultTitle, defaultBody);
};
}

private static int number(Map<String, Object> data, String key) {
if (data == null) {
return 0;
}
Object value = data.get(key);
if (value instanceof Number number) {
return number.intValue();
}
if (value instanceof String stringValue) {
try {
return Integer.parseInt(stringValue);
} catch (NumberFormatException ignored) {
return 0;
}
}
return 0;
}

private static RenderedSystemMessage message(String title, String body) {
return new RenderedSystemMessage(title, body);
}

public record RenderedSystemMessage(String title, String body) {
}
}
Loading