[Feature] system messages 구현 - #79
Conversation
# Conflicts: # manabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatRoomService.java # manabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatService.java # manabom/src/main/java/mannabom_server/manabom/domain/chat/repository/ChatRoomRepository.java
Walkthrough채팅 시스템 메시지와 푸시 전달 구조가 구조화된 이벤트·메타데이터 기반으로 변경되었습니다. 미팅 전체 취소 투표, 인증 만료 알림, 빠른 매칭 채팅방 라우팅과 상태 전이가 추가되었으며 관련 DTO, 저장소, 마이그레이션, 테스트가 포함되었습니다. ChangesStructured chat messaging and push delivery
Meeting cancellation
Meeting verification and matching
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
manabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatRoomService.java (1)
212-239: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win1:1 매칭방에서 한쪽이 나가면 상대방은 영구히 "나가기"를 못하게 됩니다.
새로 추가된
room.getChatStatus() == ChatStatus.DISABLED가드(216-218)가chatMember조회/deactivate()보다 먼저 실행됩니다.LOVEVIEW_MATCH/PROFILE_MATCH케이스(236-238)는 나갈 때room.deactivate()를 호출해 방을 DISABLED로 만드는데, 이 상태에서 상대방이 뒤늦게leaveChatRoom을 호출하면 이 가드에서 즉시IllegalStateException이 발생해 자신의ChatMember를 정리(deactivate)할 기회조차 얻지 못합니다. 결과적으로 상대방의 멤버십 레코드는 계속 ACTIVATE로 남고, 클라이언트의 "나가기" 요청은 항상 실패합니다.가드를 멤버십 정리 이후로 옮기면, 자기 멤버십 정리는 항상 성공하고 중복되는 방 상태 변경(삭제/비활성화)만 건너뛸 수 있습니다.
🐛 제안하는 수정
public void leaveChatRoom(Long roomId, Long userId){ ChatRoom room = chatRoomRepository.findById(roomId) .orElseThrow(()-> new IllegalArgumentException("채팅방 나가기: 존재하지 않는 채팅방아이디 입니다.")); - if (room.getChatStatus() == ChatStatus.DISABLED) { - throw new IllegalStateException("비활성화된 채팅방에서는 나갈 수 없습니다."); - } - ChatMember chatMember = chatMemberRepository.findByRoomIdAndUser_UserIdAndStatus(roomId, userId, ChatMemberStatus.ACTIVATE) .orElseThrow(()-> new IllegalArgumentException("방에 참여중인 유저가 아닙니다.")); chatMember.deactivate(); + if (room.getChatStatus() == ChatStatus.DISABLED) { + log.info("이미 비활성화된 채팅방의 멤버십만 정리합니다: roomId={}, userId={}", roomId, userId); + return; + } Long referenceId=null; switch (room.getType()){🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@manabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatRoomService.java` around lines 212 - 239, Move the ChatStatus.DISABLED guard in leaveChatRoom after the active ChatMember lookup and chatMember.deactivate() call, so users can always clean up their membership. Ensure subsequent room deletion or deactivation logic is skipped when the room is already disabled, while preserving the existing behavior for active rooms.
🧹 Nitpick comments (4)
manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingService.java (1)
236-253: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
joinMatchingChatRoomIfFastEntry와resolveCurrentChatRoomId의 매치 조회 로직 중복두 메서드가 거의 동일한
meetingMatchRepository.findByMeetingIdAndStatus(meeting.getId(), MatchingStatus.SUCCEEDED).orElseThrow(...)패턴을 반복하고 있고, 예외 메시지만 다릅니다("빠른 입장 미팅과..."vs"매칭된 미팅과..."). 공통 private 헬퍼로 추출하면 유지보수성이 좋아집니다.♻️ 리팩터링 제안
+ private MeetingMatch findSucceededMatchOrThrow(Long meetingId, String errorMessage) { + return meetingMatchRepository.findByMeetingIdAndStatus(meetingId, MatchingStatus.SUCCEEDED) + .orElseThrow(() -> new IllegalStateException(errorMessage)); + } private Long joinMatchingChatRoomIfFastEntry(...) { if (!isFastMatchingEntry) { return null; } - var match = meetingMatchRepository.findByMeetingIdAndStatus( - meeting.getId(), - MatchingStatus.SUCCEEDED - ) - .orElseThrow(() -> new IllegalStateException( - "빠른 입장 미팅과 연결된 성사된 매칭을 찾을 수 없습니다." - )); + var match = findSucceededMatchOrThrow(meeting.getId(), "빠른 입장 미팅과 연결된 성사된 매칭을 찾을 수 없습니다."); return chatRoomService.joinMatchingChatRoom(match, user); }Also applies to: 301-315
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingService.java` around lines 236 - 253, Extract the duplicated successful-match lookup from joinMatchingChatRoomIfFastEntry and resolveCurrentChatRoomId into a shared private helper in MeetingService. Have the helper query findByMeetingIdAndStatus with MatchingStatus.SUCCEEDED and accept the required context-specific exception message, then update both callers to use it while preserving their existing messages and behavior.manabom/src/main/java/mannabom_server/manabom/application/like/service/LikeService.java (1)
159-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
LikeService와MessageRequestService의createChatRoom이 완전히 중복. 두 서비스의 privatecreateChatRoom메서드는LikeSource/MessageSource타입만 다를 뿐 조회·분기·호출 로직이 동일합니다. 공통 헬퍼로 추출해 중복을 제거할 수 있습니다.
manabom/src/main/java/mannabom_server/manabom/application/like/service/LikeService.java#L159-L176:createChatRoom로직을 공용 헬퍼(예: 추천 이력 조회 +actorUserId전달을 캡슐화하는 별도 컴포넌트)로 위임하도록 리팩터링.manabom/src/main/java/mannabom_server/manabom/application/messageRequest/service/MessageRequestService.java#L160-L177: 동일한 공용 헬퍼를 재사용하도록 리팩터링.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@manabom/src/main/java/mannabom_server/manabom/application/like/service/LikeService.java` around lines 159 - 176, Extract the duplicated createChatRoom logic from LikeService.java lines 159-176 and MessageRequestService.java lines 160-177 into a shared helper component that handles recommendation-history lookup, source branching, and actorUserId propagation; update both services’ createChatRoom methods to delegate to it while preserving their existing LikeSource/MessageSource behavior.manabom/src/main/resources/db/migration/V26__add_structured_system_message_fields.sql (1)
8-14: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift운영 중 쓰기 차단 가능성이 있는 DDL을 온라인 배포 방식으로 분리해 주세요.
대규모 운영 테이블에서는 FK/CHECK의 즉시 검증과 일반 인덱스 생성이 전체 스캔 및 쓰기 잠금을 유발할 수 있습니다.
manabom/src/main/resources/db/migration/V26__add_structured_system_message_fields.sql#L8-L14: FK는NOT VALID후 별도 검증하고 partial index는 concurrent 생성으로 분리해 주세요.manabom/src/main/resources/db/migration/V26__add_structured_system_message_fields.sql#L20-L22: FK 추가의 검증과 잠금 영향을 배포 전략에 반영해 주세요.manabom/src/main/resources/db/migration/V22__move_meeting_cancellation_to_match.sql#L7-L25: FK/CHECK와 두 인덱스의 온라인 생성 및 트랜잭션 분리를 확인해 주세요.manabom/src/main/resources/db/migration/V23__remove_withdrawn_cancellation_status.sql#L4-L11: CHECK 검증을 데이터 정리와 분리해 잠금 시간을 줄여 주세요.manabom/src/main/resources/db/migration/V25__add_meeting_verification_failure_notification.sql#L4-L6: partial index를CREATE INDEX CONCURRENTLY로 생성할 수 있는지 확인해 주세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@manabom/src/main/resources/db/migration/V26__add_structured_system_message_fields.sql` around lines 8 - 14, 온라인 배포가 가능하도록 각 마이그레이션의 DDL을 검증·생성 단계와 트랜잭션에서 분리하세요. manabom/src/main/resources/db/migration/V26__add_structured_system_message_fields.sql#L8-L14의 fk_chat_message_actor는 NOT VALID로 추가 후 별도 검증하고 idx_chat_messages_system_event_type은 CREATE INDEX CONCURRENTLY로 생성하세요. 같은 파일 `#L20-L22의` FK 검증 및 잠금 영향도 동일한 배포 전략에 반영하세요. manabom/src/main/resources/db/migration/V22__move_meeting_cancellation_to_match.sql#L7-L25의 FK/CHECK와 두 인덱스는 온라인 생성 및 트랜잭션 분리를 적용하고, manabom/src/main/resources/db/migration/V23__remove_withdrawn_cancellation_status.sql#L4-L11에서는 CHECK 검증을 데이터 정리와 분리하세요. manabom/src/main/resources/db/migration/V25__add_meeting_verification_failure_notification.sql#L4-L6의 partial index도 CREATE INDEX CONCURRENTLY 사용과 트랜잭션 제약을 반영하세요.Source: Linters/SAST tools
manabom/src/test/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationExpirationServiceTest.java (1)
38-52: 📐 Maintainability & Code Quality | 🔵 Trivial발행된
ChatSystemMessageEvent의 내용에 대한 검증이 없습니다.
expiresPendingRequestInItsOwnProcessingStep테스트는 상태 전이만 검증하고,eventPublisher.publishEvent(...)로 전달되는 이벤트의 타입(MEETING_CANCELLATION_EXPIRED), roomId, recipients, data 내용은 검증하지 않습니다.ArgumentCaptor로 캡처해 검증을 추가하면 회귀를 더 잘 잡을 수 있습니다.✅ 제안 예시
+ ArgumentCaptor<ChatSystemMessageEvent> captor = ArgumentCaptor.forClass(ChatSystemMessageEvent.class); + verify(eventPublisher).publishEvent(captor.capture()); + assertThat(captor.getValue().getType()).isEqualTo(SystemMessageType.MEETING_CANCELLATION_EXPIRED);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@manabom/src/test/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationExpirationServiceTest.java` around lines 38 - 52, Update expiresPendingRequestInItsOwnProcessingStep to capture the ChatSystemMessageEvent passed to eventPublisher.publishEvent(...), then assert its MEETING_CANCELLATION_EXPIRED type, roomId, recipients, and data contents alongside the existing status assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@manabom/src/main/java/mannabom_server/manabom/application/chat/service/SystemMessageService.java`:
- Around line 82-118: Update recordMatchFailure to label both meetings with
MATCH_TIMED_OUT when match2Decision is AUTO_REJECTED, including the opponent
message type selection. Preserve the existing failedMeeting/opponentType
behavior for other decision combinations.
In
`@manabom/src/main/java/mannabom_server/manabom/application/meeting/dto/response/MeetingCancellationResponse.java`:
- Around line 28-54: Update MeetingCancellationResponse.of to count REJECT
decisions from votes and expose that count through a rejectedMemberCount
response field, including the builder mapping and any corresponding DTO
accessors. Preserve the existing agreed, pending, total, and vote-list behavior
so all vote decisions are represented and their counts sum to totalMemberCount.
In
`@manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationExpirationService.java`:
- Around line 42-61: MeetingCancellationExpirationService의
MEETING_CANCELLATION_EXPIRED 발행 로직을
MeetingCancellationService.expireIfNecessary()와 동일하게 맞추세요. recipients는
ChatMember가 아닌 MeetingMember 기준으로 산출하고, data에 기존 requestId/status와 함께 expiresAt을
포함하도록 수정하세요.
In
`@manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationService.java`:
- Around line 225-239: MeetingCancellationService의 expireIfNecessary()가 직접 만료
처리와 이벤트 발행을 수행하지 않도록 수정하고, 중복 로직을 담당하는
MeetingCancellationExpirationService.expire()에 위임하세요. 만료 시 해당 서비스의 단일 처리 경로와 동일한
payload가 사용되도록 기존 recipients 계산 및 publishCancellationEvent 호출을 제거하거나 통합하세요.
- Around line 158-166: In MeetingCancellationService, reorder the approval flow
so publishCancellationEvent with MEETING_CANCELLATION_APPROVED executes before
approveCancellation(request, now), including the equivalent flow referenced
around lines 204–223. Preserve the existing arguments and approval behavior
while ensuring the system message is recorded before chat rooms are disabled.
- Around line 135-142: Update vote() so it checks whether the cancellation
request is expired without mutating state before throwing for a non-PENDING
status. Move expireIfNecessary(request, now) out of the precondition path and
invoke it only when the vote can proceed, preserving the existing expiration
update and event publication without rolling it back due to the
IllegalStateException.
In
`@manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingService.java`:
- Around line 193-198: Update the enterRoomById and enterRoomByCode flows in
MeetingService so chatRoomService.joinChatRoom(meeting, user) is skipped when
isFastMatchingEntry is true, while preserving the existing join behavior for
regular entries and the subsequent matching-room flow.
In
`@manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingVerificationService.java`:
- Around line 254-267: Update the MEETING_VERIFICATION_SUCCEEDED event in
MeetingVerificationService to always use the current request’s userId as
actorUserId. Remove the fallback to verification.getStartedBy(), while
preserving the existing event payload and success flow.
In
`@manabom/src/main/java/mannabom_server/manabom/application/notification/service/NotificationService.java`:
- Around line 28-50: Separate push delivery in
NotificationService.sendNotification from the database transaction that saves
the Notification, ensuring FCM RuntimeException failures do not roll back
notification or chat-message persistence. Keep notificationRepository.save
within the existing transactional flow, and invoke pushService.sendToUser
through an independent after-commit or non-transactional failure-handling path
with logging or retry support.
In
`@manabom/src/main/java/mannabom_server/manabom/presentation/notification/controller/NotificationController.java`:
- Around line 14-29: Remove the unauthenticated sendTestNotification test
endpoint before deployment, or protect it with authentication and an explicit
administrator authorization check before invoking
notificationService.sendNotification. Ensure arbitrary targetUserId values
cannot be used by unauthenticated or non-admin callers.
In
`@manabom/src/main/resources/db/migration/V22__move_meeting_cancellation_to_match.sql`:
- Around line 1-18: Update the V22 migration to preserve existing
meeting_id-based cancellation requests by backfilling meeting_match_id through
the available meeting-to-match relationship, or add the required legacy mapping
so those rows remain discoverable by meetingMatch-based entity, repository, and
service flows. Ensure the migration satisfies chk_cancellation_request_target
and keeps existing requests eligible for cancellation voting and expiration
processing.
In
`@manabom/src/main/resources/db/migration/V23__remove_withdrawn_cancellation_status.sql`:
- Around line 1-11: Update the V23 migration before recreating
chk_cancellation_request_status to handle existing WITHDRAWN rows: convert them
to the policy-approved terminal status or add the required pre-validation that
prevents the constraint from being applied with invalid data. Ensure the
migration succeeds when legacy WITHDRAWN records exist.
In
`@manabom/src/main/resources/db/migration/V24__add_unique_active_chat_member.sql`:
- Around line 1-3: Update the V24 migration to create the partial unique index
with PostgreSQL’s concurrent index creation syntax, and configure Flyway with
spring.flyway.execute-in-transaction=false so this migration runs outside a
transaction.
---
Outside diff comments:
In
`@manabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatRoomService.java`:
- Around line 212-239: Move the ChatStatus.DISABLED guard in leaveChatRoom after
the active ChatMember lookup and chatMember.deactivate() call, so users can
always clean up their membership. Ensure subsequent room deletion or
deactivation logic is skipped when the room is already disabled, while
preserving the existing behavior for active rooms.
---
Nitpick comments:
In
`@manabom/src/main/java/mannabom_server/manabom/application/like/service/LikeService.java`:
- Around line 159-176: Extract the duplicated createChatRoom logic from
LikeService.java lines 159-176 and MessageRequestService.java lines 160-177 into
a shared helper component that handles recommendation-history lookup, source
branching, and actorUserId propagation; update both services’ createChatRoom
methods to delegate to it while preserving their existing
LikeSource/MessageSource behavior.
In
`@manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingService.java`:
- Around line 236-253: Extract the duplicated successful-match lookup from
joinMatchingChatRoomIfFastEntry and resolveCurrentChatRoomId into a shared
private helper in MeetingService. Have the helper query findByMeetingIdAndStatus
with MatchingStatus.SUCCEEDED and accept the required context-specific exception
message, then update both callers to use it while preserving their existing
messages and behavior.
In
`@manabom/src/main/resources/db/migration/V26__add_structured_system_message_fields.sql`:
- Around line 8-14: 온라인 배포가 가능하도록 각 마이그레이션의 DDL을 검증·생성 단계와 트랜잭션에서 분리하세요.
manabom/src/main/resources/db/migration/V26__add_structured_system_message_fields.sql#L8-L14의
fk_chat_message_actor는 NOT VALID로 추가 후 별도 검증하고
idx_chat_messages_system_event_type은 CREATE INDEX CONCURRENTLY로 생성하세요. 같은 파일
`#L20-L22의` FK 검증 및 잠금 영향도 동일한 배포 전략에 반영하세요.
manabom/src/main/resources/db/migration/V22__move_meeting_cancellation_to_match.sql#L7-L25의
FK/CHECK와 두 인덱스는 온라인 생성 및 트랜잭션 분리를 적용하고,
manabom/src/main/resources/db/migration/V23__remove_withdrawn_cancellation_status.sql#L4-L11에서는
CHECK 검증을 데이터 정리와 분리하세요.
manabom/src/main/resources/db/migration/V25__add_meeting_verification_failure_notification.sql#L4-L6의
partial index도 CREATE INDEX CONCURRENTLY 사용과 트랜잭션 제약을 반영하세요.
In
`@manabom/src/test/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationExpirationServiceTest.java`:
- Around line 38-52: Update expiresPendingRequestInItsOwnProcessingStep to
capture the ChatSystemMessageEvent passed to eventPublisher.publishEvent(...),
then assert its MEETING_CANCELLATION_EXPIRED type, roomId, recipients, and data
contents alongside the existing status assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f05ba588-9796-4bf8-b4cc-a2676f38a2d1
📒 Files selected for processing (75)
manabom/src/main/java/mannabom_server/manabom/application/chat/dto/event/ChatSystemMessageEvent.javamanabom/src/main/java/mannabom_server/manabom/application/chat/dto/response/ChatMessageEvent.javamanabom/src/main/java/mannabom_server/manabom/application/chat/dto/response/ChatMessageResponse.javamanabom/src/main/java/mannabom_server/manabom/application/chat/dto/response/ChatRoomListResponse.javamanabom/src/main/java/mannabom_server/manabom/application/chat/handler/ChatSystemMessageEventHandler.javamanabom/src/main/java/mannabom_server/manabom/application/chat/message/SystemMessageType.javamanabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatRoomService.javamanabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatService.javamanabom/src/main/java/mannabom_server/manabom/application/chat/service/SystemMessageService.javamanabom/src/main/java/mannabom_server/manabom/application/like/service/LikeService.javamanabom/src/main/java/mannabom_server/manabom/application/matching/service/PhotoRequestService.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/dto/request/MeetingCancellationVoteRequest.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/dto/response/MeetingCancellationResponse.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/dto/response/MeetingCancellationVoteResponse.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/scheduler/MeetingCancellationScheduler.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/scheduler/MeetingVerificationScheduler.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationExpirationService.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationService.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingMatchingService.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingService.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingVerificationExpirationService.javamanabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingVerificationService.javamanabom/src/main/java/mannabom_server/manabom/application/messageRequest/service/MessageRequestService.javamanabom/src/main/java/mannabom_server/manabom/application/notification/dto/MatchSuccessEvent.javamanabom/src/main/java/mannabom_server/manabom/application/notification/dto/SseData.javamanabom/src/main/java/mannabom_server/manabom/application/notification/handler/NotificationEventListener.javamanabom/src/main/java/mannabom_server/manabom/application/notification/service/NotificationService.javamanabom/src/main/java/mannabom_server/manabom/application/notification/service/SseService.javamanabom/src/main/java/mannabom_server/manabom/application/pushService/service/pushSender/FcmPushSender.javamanabom/src/main/java/mannabom_server/manabom/domain/chat/entity/ChatMessage.javamanabom/src/main/java/mannabom_server/manabom/domain/chat/repository/ChatMemberRepository.javamanabom/src/main/java/mannabom_server/manabom/domain/chat/repository/ChatMessageRepository.javamanabom/src/main/java/mannabom_server/manabom/domain/chat/repository/ChatRoomRepository.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/entity/Meeting.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/entity/MeetingCancellationRequest.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/entity/MeetingCancellationVote.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/entity/MeetingVerification.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/enums/CancellationVoteDecision.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/enums/MeetingCancellationStatus.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/enums/MeetingStatus.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/enums/SseEventName.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/repository/MeetingCancellationRequestRepository.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/repository/MeetingCancellationVoteRepository.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/repository/MeetingMatchRepository.javamanabom/src/main/java/mannabom_server/manabom/domain/meeting/repository/MeetingVerificationRepository.javamanabom/src/main/java/mannabom_server/manabom/domain/notification/entity/Notification.javamanabom/src/main/java/mannabom_server/manabom/domain/notification/entity/SseEventCache.javamanabom/src/main/java/mannabom_server/manabom/domain/notification/enums/NotificationType.javamanabom/src/main/java/mannabom_server/manabom/domain/notification/repository/EmitterRepository.javamanabom/src/main/java/mannabom_server/manabom/domain/notification/repository/SseEventCacheRepository.javamanabom/src/main/java/mannabom_server/manabom/infrastructure/security/websocket/StompAuthChannelInterceptor.javamanabom/src/main/java/mannabom_server/manabom/presentation/meeting/controller/MeetingController.javamanabom/src/main/java/mannabom_server/manabom/presentation/meeting/controller/MeetingMatchingController.javamanabom/src/main/java/mannabom_server/manabom/presentation/notification/controller/NotificationController.javamanabom/src/main/resources/db/migration/V21__add_meeting_cancellation_tables.sqlmanabom/src/main/resources/db/migration/V22__move_meeting_cancellation_to_match.sqlmanabom/src/main/resources/db/migration/V23__remove_withdrawn_cancellation_status.sqlmanabom/src/main/resources/db/migration/V24__add_unique_active_chat_member.sqlmanabom/src/main/resources/db/migration/V25__add_meeting_verification_failure_notification.sqlmanabom/src/main/resources/db/migration/V26__add_structured_system_message_fields.sqlmanabom/src/main/resources/db/migration/V27__persist_meeting_verification_result.sqlmanabom/src/test/java/mannabom_server/manabom/application/chat/dto/response/ChatMessageResponseTest.javamanabom/src/test/java/mannabom_server/manabom/application/chat/handler/ChatSystemMessageEventHandlerTest.javamanabom/src/test/java/mannabom_server/manabom/application/chat/message/SystemMessageTypeTest.javamanabom/src/test/java/mannabom_server/manabom/application/chat/service/ChatRoomServiceTest.javamanabom/src/test/java/mannabom_server/manabom/application/chat/service/ChatServiceTest.javamanabom/src/test/java/mannabom_server/manabom/application/chat/service/SystemMessageServiceTest.javamanabom/src/test/java/mannabom_server/manabom/application/matching/service/PhotoRequestServiceTest.javamanabom/src/test/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationExpirationServiceTest.javamanabom/src/test/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationServiceTest.javamanabom/src/test/java/mannabom_server/manabom/application/meeting/service/MeetingVerificationExpirationServiceTest.javamanabom/src/test/java/mannabom_server/manabom/application/notification/service/NotificationServiceTest.javamanabom/src/test/java/mannabom_server/manabom/domain/meeting/MeetingCancellationDomainTest.javamanabom/src/test/java/mannabom_server/manabom/domain/meeting/MeetingMemberLeaveStatusTest.javamanabom/src/test/java/mannabom_server/manabom/domain/meeting/MeetingVerificationTest.java
💤 Files with no reviewable changes (8)
- manabom/src/main/java/mannabom_server/manabom/application/notification/dto/SseData.java
- manabom/src/main/java/mannabom_server/manabom/domain/notification/entity/SseEventCache.java
- manabom/src/main/java/mannabom_server/manabom/presentation/meeting/controller/MeetingMatchingController.java
- manabom/src/main/java/mannabom_server/manabom/domain/notification/repository/SseEventCacheRepository.java
- manabom/src/main/java/mannabom_server/manabom/domain/meeting/enums/SseEventName.java
- manabom/src/main/java/mannabom_server/manabom/application/notification/handler/NotificationEventListener.java
- manabom/src/main/java/mannabom_server/manabom/domain/notification/repository/EmitterRepository.java
- manabom/src/main/java/mannabom_server/manabom/application/notification/service/SseService.java
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public List<RecordedSystemMessage> recordMatchFailure( | ||
| Long matchId, | ||
| Long actorUserId, | ||
| boolean isByTimeout | ||
| ) { | ||
| MeetingMatch match = findMatch(matchId); | ||
| Meeting failedMeeting = findFailedMeeting(match, isByTimeout); | ||
| Meeting opponentMeeting = failedMeeting.getId().equals(match.getMeeting1().getId()) | ||
| ? match.getMeeting2() | ||
| : match.getMeeting1(); | ||
|
|
||
| SystemMessageType failedTeamType = isByTimeout | ||
| ? SystemMessageType.MATCH_TIMED_OUT | ||
| : SystemMessageType.MATCH_REJECTED_BY_LEADER; | ||
| SystemMessageType opponentType = isByTimeout | ||
| ? SystemMessageType.OPPONENT_MATCH_TIMED_OUT | ||
| : SystemMessageType.MATCH_REJECTED_BY_OPPONENT; | ||
| Map<String, Object> data = Map.of("matchId", matchId, "timeout", isByTimeout); | ||
|
|
||
| return List.of( | ||
| saveForMeeting(failedMeeting, ChatSystemMessageEvent.of( | ||
| roomIdForMeeting(failedMeeting), | ||
| failedTeamType, | ||
| actorUserId, | ||
| null, | ||
| data | ||
| )), | ||
| saveForMeeting(opponentMeeting, ChatSystemMessageEvent.of( | ||
| roomIdForMeeting(opponentMeeting), | ||
| opponentType, | ||
| actorUserId, | ||
| null, | ||
| data | ||
| )) | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -B3 -A15 'AUTO_REJECTED' manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingMatchingService.javaRepository: mannabom/mannabomServer
Length of output: 161
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate relevant files =="
git ls-files | rg 'SystemMessageService\.java|MeetingMatchingService\.java|Meeting.*Decision|SystemMessageType' || true
echo
echo "== SystemMessageService outline and relevant lines =="
file="manabom/src/main/java/mannabom_server/manabom/application/chat/service/SystemMessageService.java"
wc -l "$file"
ast-grep outline "$file" || true
sed -n '1,160p' "$file"
echo
echo "== MeetingMatchingService related references =="
for f in $(git ls-files | rg 'MeetingMatchingService\.java|MatchDecision|MeetingDecision|Decision|RecordedSystemMessage|SystemMessageType'); do
echo "--- $f"
wc -l "$f"
rg -n -B3 -A15 'AUTO_REJECTED|MatchDecision|MeetingDecision|recordMatchFailure|findFailedMeeting|timed|timeout|rejected|decision' "$f" || true
done
echo
echo "== search for AUTO_REJECTED globally =="
rg -n 'AUTO_REJECTED|MatchDecision|Decision|recordMatchFailure|findFailedMeeting' . || trueRepository: mannabom/mannabomServer
Length of output: 27142
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate relevant files =="
git ls-files | rg 'SystemMessageService\.java|MeetingMatchingService\.java|Meeting.*Decision|SystemMessageType' || true
echo
echo "== SystemMessageService outline and relevant lines =="
file="manabom/src/main/java/mannabom_server/manabom/application/chat/service/SystemMessageService.java"
wc -l "$file"
ast-grep outline "$file" || true
sed -n '1,160p' "$file"
echo
echo "== MeetingMatchingService related references =="
for f in $(git ls-files | rg 'MeetingMeeting\.java|Meeting\.java|MeetingMatchingService\.java|.*Meeting.*Decision.*|Decision.*|.*RecordedSystemMessage.*|.*SystemMessageType.*'); do
echo "--- $f"
wc -l "$f"
rg -n -B3 -A15 'AUTO_REJECTED|MatchDecision|MeetingDecision|recordMatchFailure|findFailedMeeting|timed|timeout|rejected|decision' "$f" || true
done
echo
echo "== search for AUTO_REJECTED globally =="
rg -n 'AUTO_REJECTED|MatchDecision|MeetingDecision|recordMatchFailure|findFailedMeeting' . || trueRepository: mannabom/mannabomServer
Length of output: 19281
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== MeetingMatch.java =="
file="manabom/src/main/java/mannabom_server/manabom/domain/meeting/entity/MeetingMatch.java"
wc -l "$file"
cat -n "$file"
echo
echo "== MeetingMatchingService decision methods =="
file="manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingMatchingService.java"
sed -n '80,160p' "$file" | cat -n
echo
echo "== SystemMessageService failure helper =="
sed -n '260,300p' manabom/src/main/java/mannabom_server/manabom/application/chat/service/SystemMessageService.java | cat -n
echo
echo "== Event handler dispatch line =="
sed -n '25,45p' manabom/src/main/java/mannabom_server/manabom/application/chat/handler/ChatSystemMessageEventHandler.java | cat -n
echo
echo "== Tests around recordMatchFailure =="
sed -n '125,170p' manabom/src/test/java/mannabom_server/manabom/application/chat/service/SystemMessageServiceTest.java | cat -n
sed -n '35,50p' manabom/src/test/java/mannabom_server/manabom/application/chat/handler/ChatSystemMessageEventHandlerTest.java | cat -n
echo
echo "== SystemMessageType message hints =="
for label in MATCH_TIMED_OUT OPPONENT_MATCH_TIMED_OUT MATCH_REJECTED_BY_LEADER MATCH_REJECTED_BY_OPPONENT MATCH_COMPLETED MATCH_FOUND; do
echo "--- $label"
rg -n -B2 -A8 "$label" manabom/src/main/java/mannabom_server/manabom/application/chat/message/SystemMessageType.java
done
echo
echo "== Behavioral simulation of findMatch recordMatchFailure labeling =="
python3 - <<'PY'
from dataclasses import dataclass
from enum import Enum
class Decision(Enum):
WAITING=0; ACCEPTED=1; REJECTED=2; AUTO_ACCEPTED=3; AUTO_REJECTED=4
`@dataclass`(frozen=True)
class Meeting:
id: int
`@dataclass`
class Match:
meeting1: Meeting
meeting2: Meeting
meeting1Decision: Decision
meeting2Decision: Decision
def find_failed(match, isByTimeout):
failedDecision = Decision.AUTO_REJECTED if isByTimeout else Decision.REJECTED
if match.meeting1Decision == failedDecision:
return match.meeting1
if match.meeting2Decision == failedCaption := match.meeting2Decision == failedDecision:
return match.meeting2
raise Exception("no failed meeting")
for d1 in Decision:
for d2 in Decision:
match = Match(Meeting(1), Meeting(2), d1, d2)
isByTimeout = (d1 == Decision.AUTO_REJECTED and d2 == Decision.AUTO_REJECTED)
if isByTimeout:
try:
f = find_failed(match, True)
opp = Match(Meeting(2), Meeting(1), d1, d2) if f == match.meeting1 else Match(Meeting(1), Meeting(2), d1, d2)
fail_type = "MATCH_TIMED_OUT"
opp_type = "OPPONENT_MATCH_TIMED_OUT"
print(f"d1={d1.name}, d2={d2.name} -> failed={f.id}, opponent={opp.id} fail_type={fail_type}, opponent_type={opp_type}")
except Exception as e:
print(f"d1={d1.name}, d2={d2.name} -> exception={e}")
PYRepository: mannabom/mannabomServer
Length of output: 21648
동시 타임아웃 시 양측 메시지 라벨링을 모두 MATCH_TIMED_OUT으로 보내도록 수정하세요.
findFailedMeeting은 meeting1Decision을 먼저 확인하고, 나머지 한 팀은 상대 쪽 메시지로 항상 라벨링하지만 MeetingMatch.processExpiration은 timeout/리젝 카운트가 남아있는 팀마다 각각 AUTO_REJECTED로 진화합니다. 따라서 양측 모두 자동으로 거절되어야 한다면 match2Decision == AUTO_REJECTED인 경우 상대방 라벨링도 MATCH_TIMED_OUT 처리를 해주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@manabom/src/main/java/mannabom_server/manabom/application/chat/service/SystemMessageService.java`
around lines 82 - 118, Update recordMatchFailure to label both meetings with
MATCH_TIMED_OUT when match2Decision is AUTO_REJECTED, including the opponent
message type selection. Preserve the existing failedMeeting/opponentType
behavior for other decision combinations.
| public static MeetingCancellationResponse of( | ||
| MeetingCancellationRequest request, | ||
| List<MeetingCancellationVote> votes | ||
| ) { | ||
| int agreed = (int) votes.stream() | ||
| .filter(vote -> vote.getDecision() == CancellationVoteDecision.AGREE) | ||
| .count(); | ||
| int pending = (int) votes.stream() | ||
| .filter(vote -> vote.getDecision() == CancellationVoteDecision.PENDING) | ||
| .count(); | ||
|
|
||
| return MeetingCancellationResponse.builder() | ||
| .requestId(request.getId()) | ||
| .matchId(request.getMeetingMatch().getId()) | ||
| .initiatorUserId(request.getInitiator().getUserId()) | ||
| .status(request.getStatus()) | ||
| .requestedAt(request.getRequestedAt()) | ||
| .expiresAt(request.getExpiresAt()) | ||
| .completedAt(request.getCompletedAt()) | ||
| .totalMemberCount(votes.size()) | ||
| .agreedMemberCount(agreed) | ||
| .pendingMemberCount(pending) | ||
| .votes(votes.stream() | ||
| .map(MeetingCancellationVoteResponse::from) | ||
| .toList()) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
REJECT 투표가 집계에서 누락됨.
agreedMemberCount와 pendingMemberCount만 계산하고 REJECT 투표는 어디에도 포함되지 않습니다. MeetingCancellationService.vote()는 REJECT 발생 즉시 요청을 종료시키므로, 이 시점 votes에는 REJECT 1건 + 나머지 PENDING이 섞여 있을 수 있어 agreedMemberCount + pendingMemberCount != totalMemberCount가 되는 상황이 발생합니다. 클라이언트가 이 응답만으로 투표 현황을 재구성하면 REJECT 투표가 사라진 것처럼 보일 수 있습니다.
💡 제안: rejectedMemberCount 추가
private int totalMemberCount;
private int agreedMemberCount;
private int pendingMemberCount;
+ private int rejectedMemberCount;
private List<MeetingCancellationVoteResponse> votes;
public static MeetingCancellationResponse of(
MeetingCancellationRequest request,
List<MeetingCancellationVote> votes
) {
int agreed = (int) votes.stream()
.filter(vote -> vote.getDecision() == CancellationVoteDecision.AGREE)
.count();
int pending = (int) votes.stream()
.filter(vote -> vote.getDecision() == CancellationVoteDecision.PENDING)
.count();
+ int rejected = (int) votes.stream()
+ .filter(vote -> vote.getDecision() == CancellationVoteDecision.REJECT)
+ .count();
return MeetingCancellationResponse.builder()
...
.agreedMemberCount(agreed)
.pendingMemberCount(pending)
+ .rejectedMemberCount(rejected)
...📝 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.
| public static MeetingCancellationResponse of( | |
| MeetingCancellationRequest request, | |
| List<MeetingCancellationVote> votes | |
| ) { | |
| int agreed = (int) votes.stream() | |
| .filter(vote -> vote.getDecision() == CancellationVoteDecision.AGREE) | |
| .count(); | |
| int pending = (int) votes.stream() | |
| .filter(vote -> vote.getDecision() == CancellationVoteDecision.PENDING) | |
| .count(); | |
| return MeetingCancellationResponse.builder() | |
| .requestId(request.getId()) | |
| .matchId(request.getMeetingMatch().getId()) | |
| .initiatorUserId(request.getInitiator().getUserId()) | |
| .status(request.getStatus()) | |
| .requestedAt(request.getRequestedAt()) | |
| .expiresAt(request.getExpiresAt()) | |
| .completedAt(request.getCompletedAt()) | |
| .totalMemberCount(votes.size()) | |
| .agreedMemberCount(agreed) | |
| .pendingMemberCount(pending) | |
| .votes(votes.stream() | |
| .map(MeetingCancellationVoteResponse::from) | |
| .toList()) | |
| .build(); | |
| } | |
| public static MeetingCancellationResponse of( | |
| MeetingCancellationRequest request, | |
| List<MeetingCancellationVote> votes | |
| ) { | |
| int agreed = (int) votes.stream() | |
| .filter(vote -> vote.getDecision() == CancellationVoteDecision.AGREE) | |
| .count(); | |
| int pending = (int) votes.stream() | |
| .filter(vote -> vote.getDecision() == CancellationVoteDecision.PENDING) | |
| .count(); | |
| int rejected = (int) votes.stream() | |
| .filter(vote -> vote.getDecision() == CancellationVoteDecision.REJECT) | |
| .count(); | |
| return MeetingCancellationResponse.builder() | |
| .requestId(request.getId()) | |
| .matchId(request.getMeetingMatch().getId()) | |
| .initiatorUserId(request.getInitiator().getUserId()) | |
| .status(request.getStatus()) | |
| .requestedAt(request.getRequestedAt()) | |
| .expiresAt(request.getExpiresAt()) | |
| .completedAt(request.getCompletedAt()) | |
| .totalMemberCount(votes.size()) | |
| .agreedMemberCount(agreed) | |
| .pendingMemberCount(pending) | |
| .rejectedMemberCount(rejected) | |
| .votes(votes.stream() | |
| .map(MeetingCancellationVoteResponse::from) | |
| .toList()) | |
| .build(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@manabom/src/main/java/mannabom_server/manabom/application/meeting/dto/response/MeetingCancellationResponse.java`
around lines 28 - 54, Update MeetingCancellationResponse.of to count REJECT
decisions from votes and expose that count through a rejectedMemberCount
response field, including the builder mapping and any corresponding DTO
accessors. Preserve the existing agreed, pending, total, and vote-list behavior
so all vote decisions are represented and their counts sum to totalMemberCount.
| ChatRoom room = chatRoomRepository.findByMatch(request.getMeetingMatch()) | ||
| .orElseThrow(() -> new IllegalStateException("매칭 채팅방이 존재하지 않습니다.")); | ||
| List<Long> recipients = chatMemberRepository | ||
| .findAllByRoomIdAndStatus(room.getId(), ChatMemberStatus.ACTIVATE) | ||
| .stream() | ||
| .map(ChatMember::getUser) | ||
| .map(user -> user.getUserId()) | ||
| .toList(); | ||
| Map<String, Object> data = new HashMap<>(); | ||
| if (request.getId() != null) { | ||
| data.put("requestId", request.getId()); | ||
| } | ||
| data.put("status", request.getStatus().name()); | ||
| eventPublisher.publishEvent(ChatSystemMessageEvent.of( | ||
| room.getId(), | ||
| SystemMessageType.MEETING_CANCELLATION_EXPIRED, | ||
| null, | ||
| recipients, | ||
| data | ||
| )); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major
MEETING_CANCELLATION_EXPIRED 이벤트 payload/수신자 계산 방식이 다른 경로와 불일치합니다.
MeetingCancellationService.expireIfNecessary()에서 발행하는 동일 이벤트(MEETING_CANCELLATION_EXPIRED)와 recipients 산출 기준(ChatMember vs MeetingMember)과 data 필드(expiresAt 누락)가 다릅니다. 자세한 내용은 통합 코멘트를 참고해주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationExpirationService.java`
around lines 42 - 61, MeetingCancellationExpirationService의
MEETING_CANCELLATION_EXPIRED 발행 로직을
MeetingCancellationService.expireIfNecessary()와 동일하게 맞추세요. recipients는
ChatMember가 아닌 MeetingMember 기준으로 산출하고, data에 기존 requestId/status와 함께 expiresAt을
포함하도록 수정하세요.
| MeetingCancellationRequest request = requestRepository.findByIdForUpdate(requestId) | ||
| .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 미팅 취소 요청입니다.")); | ||
| Instant now = Instant.now(); | ||
| expireIfNecessary(request, now); | ||
|
|
||
| if (request.getStatus() != MeetingCancellationStatus.PENDING) { | ||
| throw new IllegalStateException("이미 종료된 미팅 취소 요청입니다."); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
vote()에서 만료 처리 후 예외를 던지면 트랜잭션이 롤백되어 상태 변경이 무효화됩니다.
expireIfNecessary(request, now)가 request.expire(now)로 상태를 변경하고 이벤트를 발행한 직후, 상태가 PENDING이 아니면 IllegalStateException을 던집니다. @Transactional 기본 롤백 규칙상 RuntimeException이 전파되면 트랜잭션 전체(만료 상태 변경 포함)가 롤백되어, 실제 DB 상태는 여전히 PENDING인데 클라이언트에게는 오류만 반환됩니다. 이후 요청은 스케줄러가 다시 처리할 때까지 실제로 만료되지 않습니다.
상태 변경을 유발하는 expireIfNecessary 호출을 던지기 직전에 두지 말고, 부작용 없이 상태만 확인하도록 변경하는 것을 제안합니다.
🐛 제안하는 수정
MeetingCancellationRequest request = requestRepository.findByIdForUpdate(requestId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 미팅 취소 요청입니다."));
Instant now = Instant.now();
- expireIfNecessary(request, now);
-
- if (request.getStatus() != MeetingCancellationStatus.PENDING) {
+ if (request.getStatus() != MeetingCancellationStatus.PENDING || request.isExpiredAt(now)) {
throw new IllegalStateException("이미 종료된 미팅 취소 요청입니다.");
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationService.java`
around lines 135 - 142, Update vote() so it checks whether the cancellation
request is expired without mutating state before throwing for a non-PENDING
status. Move expireIfNecessary(request, now) out of the precondition path and
invoke it only when the vote can proceed, preserving the existing expiration
update and event publication without rolling it back due to the
IllegalStateException.
| } else if (allMembersAgreed(requestId)) { | ||
| approveCancellation(request, now); | ||
| publishCancellationEvent( | ||
| request, | ||
| SystemMessageType.MEETING_CANCELLATION_APPROVED, | ||
| userId, | ||
| recipientUserIds | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate files"
fd -a 'MeetingCancellationService\.java|ChatSystemMessageEventHandler\.java|.*Chat.*\.java|.*SystemMessage.*\.java' . | sed 's#^\./##'
echo
echo "Git diff stat"
git diff --stat || true
echo
echo "Outline MeetingCancellationService"
ast-grep outline manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationService.java --view compact || true
echo
echo "Relevant sections"
sed -n '120,240p' manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationService.java
echo
echo "Search deactivate and handler"
rg -n "deactivate\(|ChatSystemMessageEventHandler|MEETING_CANCELLATION_APPROVED|publishCancellationEvent|nonActive|inactive|active" -S .Repository: mannabom/mannabomServer
Length of output: 20206
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "MeetingCancellationService full relevant methods"
sed -n '1,350p' manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationService.java | nl -ba
echo
echo "ChatSystemMessageEventHandler"
sed -n '1,220p' manabom/src/main/java/mannabom_server/manabom/application/chat/handler/ChatSystemMessageEventHandler.java | nl -ba
echo
echo "ChatRoom deactivate and active getter"
sed -n '90,135p' manabom/src/main/java/mannabom_server/manabom/domain/chat/entity/ChatRoom.java | nl -ba
echo
echo "ChatMember active/deactivate"
sed -n '1,90p' manabom/src/main/java/mannabom_server/manabom/domain/chat/entity/ChatMember.java | nl -ba
echo
echo "Chat room/service deactivate usages"
sed -n '150,240p' manabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatRoomService.java | nl -ba
echo
echo "Test inactive-related behavior"
sed -n '1,220p' manabom/src/test/java/mannabom_server/manabom/application/chat/handler/ChatSystemMessageEventHandlerTest.java | nl -baRepository: mannabom/mannabomServer
Length of output: 250
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "MeetingCancellationService methods"
sed -n '1,350p' manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationService.java | cat -n
echo
echo "ChatSystemMessageEventHandler"
sed -n '1,220p' manabom/src/main/java/mannabom_server/manabom/application/chat/handler/ChatSystemMessageEventHandler.java | cat -n
echo
echo "ChatRoom deactivate and active getter"
sed -n '90,135p' manabom/src/main/java/mannabom_server/manabom/domain/chat/entity/ChatRoom.java | cat -n
echo
echo "ChatMember active/deactivate"
sed -n '1,90p' manabom/src/main/java/mannabom_server/manabom/domain/chat/entity/ChatMember.java | cat -n
echo
echo "ChatRoomService deactivate usages"
sed -n '150,240p' manabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatRoomService.java | cat -n
echo
echo "ChatSystemMessageEventHandler tests"
sed -n '1,220p' manabom/src/test/java/mannabom_server/manabom/application/chat/handler/ChatSystemMessageEventHandlerTest.java | cat -nRepository: mannabom/mannabomServer
Length of output: 30014
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "SystemMessageService methods/search"
rg -n "class SystemMessageService|recordForRoom|record.*Room|dispatch|ChatRoomRepository|chatRoomRepository|ChatStatus|disabled|active|ACTIVE|DISAB" manabom/src/main/java/mannabom_server/manabom/application/chat/service/SystemMessageService.java manabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatRoomService.java manabom/src/main/java/mannabom_server/manabom/application/chat/message -S
sed -n '1,360p' manabom/src/main/java/mannabom_server/manabom/application/chat/service/SystemMessageService.java | cat -nRepository: mannabom/mannabomServer
Length of output: 19656
채팅방 비활성화 전에 승인 시스템 메시지를 발행하세요.
approveCancellation(request, now)에서 매칭 채팅방과 두 미팅 채팅방을 disbale() 처리한 뒤 MEETING_CANCELLATION_APPROVED 이벤트가 발행됩니다. 시스템 메시지를 비활성 채팅방에 기록해 푸쉬 실패 로직까지 실행한다면, 승인 안내가 사용자 눈에 충분히 들어오지 않을 수 있습니다. 이벤트 발행을 비활성화 호출보다 앞에 두는 순서가 더 안전합니다.
Also applies to lines 204-223.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationService.java`
around lines 158 - 166, In MeetingCancellationService, reorder the approval flow
so publishCancellationEvent with MEETING_CANCELLATION_APPROVED executes before
approveCancellation(request, now), including the equivalent flow referenced
around lines 204–223. Preserve the existing arguments and approval behavior
while ensuring the system message is recorded before chat rooms are disabled.
| @Transactional | ||
| public void sendMatchFound(Long matchId, Instant decisionDeadLine){ | ||
| MeetingMatch match = findMatch(matchId); | ||
| String timeStr = formatTime(decisionDeadLine); | ||
|
|
||
| String title = "두근두근 새 매칭 도착! 💘"; | ||
| String message = timeStr + "까지 수락 여부를 결정해주세요."; | ||
|
|
||
| Map<String, Object> data = new HashMap<>(); | ||
| data.put("matchId", matchId); | ||
| data.put("deadline", decisionDeadLine.toString()); | ||
|
|
||
|
|
||
|
|
||
|
|
||
| broadcastToMeetingMembers(match.getMeeting1().getId(),SseEventName.MATCH_FOUND,title, message,data); | ||
| broadcastToMeetingMembers(match.getMeeting2().getId(),SseEventName.MATCH_FOUND,title, message,data); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void sendMatchSuccess(Long matchId, Long chatRoomId,boolean isByTimeout){ | ||
| MeetingMatch match = findMatch(matchId); | ||
|
|
||
| Map<String,Object> data = new HashMap<>(); | ||
| data.put("matchId",matchId); | ||
| data.put("chatRoomId",chatRoomId); | ||
|
|
||
| sendMatchSuccessToTeam(match.getMeeting1(),match.getMeeting1Decision(),isByTimeout,data); | ||
| sendMatchSuccessToTeam(match.getMeeting2(),match.getMeeting2Decision(),isByTimeout,data); | ||
|
|
||
| } | ||
| private void sendMatchSuccessToTeam(Meeting meeting, MeetingDecision decision, boolean isByTimeout, Map<String, Object> data){ | ||
| String title, message; | ||
| if(decision==MeetingDecision.AUTO_ACCEPTED){ | ||
| if(isByTimeout){ | ||
| title = "매칭 자동 성사 ⏰"; | ||
| message = "시간 초과 및 거절권 부족으로 자동 수락되었습니다. 채팅으로 대화를 나눠보세요"; | ||
| }else { | ||
| title = "매칭 발견 및 성사 🎉"; | ||
| message = "거절권 부족으로 자동 수락되었습니다. 채팅으로 대화를 나눠보세요"; | ||
| } | ||
| } | ||
| else{ | ||
| title = "매칭 성사! 🎉"; | ||
| message = "상대방의 수락으로 매칭이 성사되었습니다! 채팅으로 대화를 나눠보세요"; | ||
| } | ||
| broadcastToMeetingMembers(meeting.getId(),SseEventName.MATCHING_COMPLETED,title, message,data); | ||
| } | ||
|
|
||
|
|
||
| @Transactional | ||
| public void sendMatchFailure(Long matchId, Long triggerUserId, boolean isByTimeout){ | ||
| MeetingMatch match = findMatch(matchId); | ||
|
|
||
| Map<String,Object> data = new HashMap<>(); | ||
| data.put("matchId",matchId); | ||
|
|
||
| String title = "매칭 실패 😢"; | ||
| String message; | ||
| if(isByTimeout){ | ||
| boolean isMeeting1Timeout = match.getMeeting1Decision()==MeetingDecision.AUTO_REJECTED; | ||
| Long timeoutTeam = isMeeting1Timeout ? match.getMeeting1().getId() : match.getMeeting2().getId(); | ||
| Long opponent = isMeeting1Timeout ? match.getMeeting2().getId() : match.getMeeting1().getId(); | ||
|
|
||
| message = "시간 안에 응답하지 않아. 자동으로 거절되었습니다. 더 좋은 인연을 찾아볼게요!"; | ||
| broadcastToMeetingMembers(timeoutTeam,SseEventName.MATCHING_FAILED,title,message,data); | ||
| message= "상대방의 무응답으로 매칭이 취소되었습니다.더 좋은 인연을 찾아볼게요!"; | ||
| broadcastToMeetingMembers(opponent,SseEventName.MATCHING_FAILED,title,message,data); | ||
| } | ||
| else { | ||
| boolean didMeeting1Reject = isMemberOfMeeting(match.getMeeting1().getId(), triggerUserId); Long rejectTeam = didMeeting1Reject ? match.getMeeting1().getId() : match.getMeeting2().getId(); | ||
| Long opponent = didMeeting1Reject ? match.getMeeting2().getId() : match.getMeeting1().getId(); | ||
|
|
||
| message = "우리 팀 리더가 매칭을 거절했습니다.더 좋은 인연을 찾아볼게요!"; | ||
| broadcastToMeetingMembersExcept(rejectTeam,triggerUserId,SseEventName.MATCHING_FAILED,title,message,data); | ||
| message= "상대방의 거절로 매칭이 취소되었습니다. 더 좋은 인연을 찾아볼게요!" ; | ||
| broadcastToMeetingMembers(opponent,SseEventName.MATCHING_FAILED,title,message,data); | ||
| } | ||
| } | ||
|
|
||
| @Transactional | ||
| public void sendNotification(Long userId, SseEventName event, String title, String message, Object data) { | ||
| String jsonData = null; | ||
| try{ | ||
| if(data!=null) | ||
| jsonData= objectMapper.writeValueAsString(data); | ||
| } | ||
| catch (JsonProcessingException e) { | ||
| jsonData = "{}"; | ||
| } | ||
| public void sendNotification( | ||
| Long userId, | ||
| NotificationType type, | ||
| String title, | ||
| String message, | ||
| Object data | ||
| ) { | ||
| Notification notification = Notification.builder() | ||
| .userId(userId) | ||
| .title(title) | ||
| .message(message) | ||
| .type(event) | ||
| .data(jsonData) | ||
| .type(type) | ||
| .data(toJson(data)) | ||
| .isRead(false) | ||
| .build(); | ||
| notificationRepository.save(notification); | ||
|
|
||
| if (!sseService.send(userId, event, new SseData(title, message, data))) { | ||
|
|
||
| log.info("유저({})가 오프라인이므로 Push 알림을 전송합니다.", userId); | ||
| PushMessage pushMessage = new PushMessage(title, message, convertObjectToMap(event, data)); | ||
| pushService.sendToUser(userId, pushMessage); | ||
| } | ||
| pushService.sendToUser( | ||
| userId, | ||
| new PushMessage(title, message, convertObjectToMap(type, data)) | ||
| ); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
ast-grep outline manabom/src/main/java/mannabom_server/manabom/application/pushService/service/pushSender/FcmPushSender.java --items all
rg -n -B3 -A15 'class FcmPushSender' manabom/src/main/java/mannabom_server/manabom/application/pushService/service/pushSender/FcmPushSender.javaRepository: mannabom/mannabomServer
Length of output: 1565
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map and inspect relevant service files without running repository code.
for f in \
manabom/src/main/java/mannabom_server/manabom/application/notification/service/NotificationService.java \
manabom/src/main/java/mannabom_server/manabom/application/pushService/service/pushSender/FcmPushSender.java \
manabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatService.java \
manabom/src/main/java/mannabom_server/manabom/application/systemMessage/service/SystemMessageService.java
do
if [ -f "$f" ]; then
echo "===== $f ====="
wc -l "$f"
ast-grep outline "$f" --items all || true
echo "---"
fi
done
echo "===== FCM Sender relevant lines ====="
cat -n manabom/src/main/java/mannabom_server/manabom/application/pushService/service/pushSender/FcmPushSender.java | sed -n '1,220p'
echo "===== NotificationService relevant lines ====="
cat -n manabom/src/main/java/mannabom_server/manabom/application/notification/service/NotificationService.java | sed -n '1,120p'
echo "===== ChatService notification/save usages ====="
rg -n -B5 -A8 'sendPushToMembersOutsideRoom|sendMessage\(|notificationService\.sendNotification|chatMessageRepository\.save|`@Transactional`' \
manabom/src/main/java/mannabom_server/manabom/application/chat/service/ChatService.java || true
echo "===== SystemMessage fallback relevant lines ====="
cat -n manabom/src/main/java/mannabom_server/manabom/application/systemMessage/service/SystemMessageService.java | sed -n '1,140p'
echo "===== Search all sendNotification callers ====="
rg -n -B3 -A5 'notificationService\.sendNotification|sendNotification\(' manabom/src/main/java || trueRepository: mannabom/mannabomServer
Length of output: 17953
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path("manabom/src/main/java/mannabom_server/manabom/application/pushService/service/pushSender/FcmPushSender.java")
text = p.read_text()
checks = {
"try_block_near_sendToToken": "\n\ttry {\n\t\tFirebaseMessaging.getInstance(firebaseApp);" in text,
"catch_block_near_sendToToken": "catch (FirebaseMessagingException e)" in text,
"throw_in_sendToToken_has_catch": False,
"sendToToken_has_return_value": "public void sendToToken" in text and text[text.index("public void sendToToken"):text.index("}\n}\n", text.index("public void sendToToken"))].find("catch") == -1,
}
for key in range(text.index("public void sendToToken"), text.index("}", text.index("public void sendToToken"))+1):
if text[key:index]
PYRepository: mannabom/mannabomServer
Length of output: 262
Push 발송 실패가 알림 저장(및 채팅 메시지 저장)까지 롤백시키지 않도록 분리하세요.
sendNotification()은 notificationRepository.save(...)와 pushService.sendToUser(...)를 같은 트랜잭션 경계에서 수행합니다. pushService.sendToUser()가 FcmPushSender.sendToToken()에서 FCM 호출 실패를 RuntimeException으로 다시 던지기 때문에, 이 예외가 전파되면 저장된 Notification이 롤백되고 ChatService.sendMessage()에서는 상단 트랜잭션까지 취소되어 chatMessageRepository.save(message)까지 사라질 수 있습니다.
Push 발송은 DB 저장과 트랜잭션적으로 분리하고, 실패 시 로그/재시도 등 별도 경로에서 관리하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@manabom/src/main/java/mannabom_server/manabom/application/notification/service/NotificationService.java`
around lines 28 - 50, Separate push delivery in
NotificationService.sendNotification from the database transaction that saves
the Notification, ensuring FCM RuntimeException failures do not roll back
notification or chat-message persistence. Keep notificationRepository.save
within the existing transactional flow, and invoke pushService.sendToUser
through an independent after-commit or non-transactional failure-handling path
with logging or retry support.
| /** | ||
| * 배포 전 삭제 | ||
| * **/ | ||
| @PostMapping("/test/send") | ||
| public String sendTestNotification(@RequestBody TestNotificationRequest request) { | ||
|
|
||
| // 아까 만든 통합 알림 메서드 호출 (DB저장 + SSE전송 + 실패시 Push) | ||
| notificationService.sendNotification( | ||
| request.targetUserId(), | ||
| SseEventName.MATCH_FOUND, // 테스트용 이벤트 타입 | ||
| NotificationType.MATCH_FOUND, | ||
| request.title(), | ||
| request.body(), | ||
| null // 데이터는 일단 null (필요하면 객체 넣어도 됨) | ||
| ); | ||
|
|
||
| return "전송 완료! (SSE 연결된 탭을 확인하세요)"; | ||
| return "Push 전송 완료"; | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
임의 사용자에게 push를 보낼 수 있는 미인증 테스트 엔드포인트.
/api/notification/test/send에 인증/인가 검증이 없어 targetUserId를 임의로 지정해 누구에게든 push를 발송할 수 있습니다. 주석대로 배포 전 삭제 또는 관리자 권한 제한이 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@manabom/src/main/java/mannabom_server/manabom/presentation/notification/controller/NotificationController.java`
around lines 14 - 29, Remove the unauthenticated sendTestNotification test
endpoint before deployment, or protect it with authentication and an explicit
administrator authorization check before invoking
notificationService.sendNotification. Ensure arbitrary targetUserId values
cannot be used by unauthenticated or non-admin callers.
| ALTER TABLE meeting_cancellation_requests | ||
| ADD COLUMN meeting_match_id BIGINT; | ||
|
|
||
| ALTER TABLE meeting_cancellation_requests | ||
| ALTER COLUMN meeting_id DROP NOT NULL; | ||
|
|
||
| ALTER TABLE meeting_cancellation_requests | ||
| ADD CONSTRAINT fk_cancellation_request_meeting_match | ||
| FOREIGN KEY (meeting_match_id) | ||
| REFERENCES meeting_matches (id); | ||
|
|
||
| ALTER TABLE meeting_cancellation_requests | ||
| ADD CONSTRAINT chk_cancellation_request_target | ||
| CHECK ( | ||
| (meeting_id IS NOT NULL AND meeting_match_id IS NULL) | ||
| OR | ||
| (meeting_id IS NULL AND meeting_match_id IS NOT NULL) | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== tracked migration files =="
git ls-files | rg 'manabom/src/main/resources/db/migration.*(V2[0-2].*|.*meeting.*)' || true
echo
echo "== candidate files =="
fd -a '(V22__move_meeting_cancellation_to_match.sql|MeetingCancellationRequest|meeting_cancellation_requests|meeting_matches|V21)' . | sed 's#^\./##' | head -200
echo
echo "== V22 contents =="
cat -n manabom/src/main/resources/db/migration/V22__move_meeting_cancellation_to_match.sql
echo
echo "== related tables/migrations containing meeting_cancellation_requests =="
rg -n "meeting_cancellation_requests|meeting_cancellation|meeting_matches|meeting_id|meeting_match_id|cancel|Cancellation" manabom/src/main/resources/db/migration -S || trueRepository: mannabom/mannabomServer
Length of output: 9568
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Java/Kotlin files matching cancellation request / meeting match =="
git ls-files | rg 'manabom/src/main/(java|kotlin)' | xargs rg -n "MeetingCancellationRequest|meeting_match_id|meetingMatch|MeetingMatch|MeetingCancellationRequest" || true
echo
echo "== entity outlines for matching files =="
for f in $(git ls-files | rg 'manabom/src/main/(java|kotlin)' | xargs rg -l "MeetingCancellationRequest|meetingMatch|MeetingMatch" || true); do
echo "--- $f"
wc -l "$f"
sed -n '1,220p' "$f"
doneRepository: mannabom/mannabomServer
Length of output: 50380
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== git diff summary for PR context =="
git diff --stat HEAD..origin/main 2>/dev/null || git diff --stat HEAD 2>/dev/null || true
echo
echo "== current migration diffs if any =="
git diff -- manabom/src/main/resources/db/migration/V22__move_meeting_cancellation_to_match.sql 2>/dev/null || trueRepository: mannabom/mannabomServer
Length of output: 237
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== cancellation/request repository methods =="
cat -n manabom/src/main/java/mannabom_server/manabom/domain/meeting/repository/MeetingCancellationRequestRepository.java
echo
echo "== cancellation service relevant methods =="
cat -n manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationService.java | sed -n '1,330p'
echo
echo "== expiration service relevant methods =="
cat -n manabom/src/main/java/mannabom_server/manabom/application/meeting/service/MeetingCancellationExpirationService.java | sed -n '1,120p'
echo
echo "== vote entity and vote repository =="
cat -n manabom/src/main/java/mannabom_server/manabom/domain/meeting/entity/MeetingCancellationVote.java | sed -n '1,140p'
rg -n "MeetingCancellationVote|meetingCancellationVotes|requestId|meetings" manabom/src/main/java/mannabom_server/manabom/domain/meeting manabom/src/main/java/mannabom_server/manabom/application/meeting -S || true
echo
echo "== deterministic migration/application invariant check =="
python3 - <<'PY'
from pathlib import Path
import re
v22 = Path("manabom/src/main/resources/db/migration/V22__move_meeting_cancellation_to_match.sql").read_text()
repo = Path("manabom/src/main/java/mannabom_server/manabom/domain/meeting/repository/MeetingCancellationRequestRepository.java").read_text()
entity = Path("manabom/src/main/java/mannabom_server/manabom/domain/meeting/entity/MeetingCancellationRequest.java").read_text()
service = Path("manabom/src/main/java/mannabom_server/manabom/domain/meeting/service/MeetingCancellationService.java").read_text()
exp = Path("manabom/src/main/java/mannabom_server/manabom/domain/meeting/service/MeetingCancellationExpirationService.java").read_text()
checks = {
"v22 adds meeting_match_id": re.search(r"ADD\s+COLUMN\s+meeting_match_id\s+BIGINT", v22) is not None,
"v22 drops meeting_id not null": "DROP NOT NULL" in v22 and "ALTER COLUMN meeting_id" in v22,
"v22 has no UPDATE or INSERT backfill": not re.search(r"\bUPDATE\b", v22, re.IGNORECASE),
"entity only maps meeting_match_id": "`@JoinColumn`(name = \"meeting_match_id\")" in entity and '`@JoinColumn`(name = "meeting_id")' not in entity,
"repo exists by match": "existsByMeetingMatch_IdAndStatus" in repo,
"repo find by match": "findByMeetingMatch_IdAndStatus" in repo,
"service creates by match": "MeetingCancellationRequest.create(" in service and "getMeetingMatch" in service,
"expiration gets by match": "getMeetingMatch" in exp,
}
for name, ok in checks.items():
print(f"{name}: {ok}")
PYRepository: mannabom/mannabomServer
Length of output: 32353
기존 meeting_id 기반 취소 요청을 meeting_match_id로 backfill하거나 레거시 매핑을 추가하세요.
V22는 meeting_match_id를 추가하지만 기존 V21 행에는 값을 대입하지 않습니다. 현재 엔티티 · 저장소 · 서비스의 모든 취소 요청 조회가 meetingMatch 기준으로 처리되므로, 기존 데이터는 meetingMatch == null 상태로 남아 진행 중인 취소 투표나 만료 처리가 누락/실패할 수 있습니다.
🧰 Tools
🪛 Squawk (2.59.0)
[warning] 5-5: Dropping a NOT NULL constraint may break existing clients.
(ban-drop-not-null)
[warning] 8-10: By default new constraints require a table scan and block writes to the table while that scan occurs. Use NOT VALID with a later VALIDATE CONSTRAINT call.
(constraint-missing-not-valid)
[warning] 8-10: Adding a foreign key constraint requires a table scan and a SHARE ROW EXCLUSIVE lock on both tables, which blocks writes to each table. Add NOT VALID to the constraint in one transaction and then VALIDATE the constraint in a separate transaction.
(adding-foreign-key-constraint)
[warning] 13-18: By default new constraints require a table scan and block writes to the table while that scan occurs. Use NOT VALID with a later VALIDATE CONSTRAINT call.
(constraint-missing-not-valid)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@manabom/src/main/resources/db/migration/V22__move_meeting_cancellation_to_match.sql`
around lines 1 - 18, Update the V22 migration to preserve existing
meeting_id-based cancellation requests by backfilling meeting_match_id through
the available meeting-to-match relationship, or add the required legacy mapping
so those rows remain discoverable by meetingMatch-based entity, repository, and
service flows. Ensure the migration satisfies chk_cancellation_request_target
and keeps existing requests eligible for cancellation voting and expiration
processing.
| ALTER TABLE meeting_cancellation_requests | ||
| DROP CONSTRAINT chk_cancellation_request_status; | ||
|
|
||
| ALTER TABLE meeting_cancellation_requests | ||
| ADD CONSTRAINT chk_cancellation_request_status | ||
| CHECK (status IN ( | ||
| 'PENDING', | ||
| 'APPROVED', | ||
| 'REJECTED', | ||
| 'EXPIRED' | ||
| )); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate migration files =="
fd -a 'V[0-9]+__.*sql$' . | sed 's#^\./##' | rg 'meeting|cancellation|WITHDRAWN|V21|V23' || true
echo "== V23 file =="
if [ -f manabom/src/main/resources/db/migration/V23__remove_withdrawn_cancellation_status.sql ]; then
cat -n manabom/src/main/resources/db/migration/V23__remove_withdrawn_cancellation_status.sql
fi
echo "== surrounding migrations mentioning WITHDRAWN =="
rg -n "WITHDRAWN|withdrawn|chk_cancellation_request_status|meeting_cancellation_requests|cancellation.*status" manabom/src/main/resources/db/migration || true
echo "== Java enum/status classes =="
rg -n "WITHDRAWN|Withdrawn|cancelled|CANCELLED|CANCELLATION_REQUEST|status" -g '*.java' manabom || trueRepository: mannabom/mannabomServer
Length of output: 41034
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== V21 table schema =="
cat -n manabom/src/main/resources/db/migration/V21__add_meeting_cancellation_tables.sql | sed -n '1,80p'
echo "== V22 migration =="
cat -n manabom/src/main/resources/db/migration/V22__move_meeting_cancellation_to_match.sql | sed -n '1,120p'
echo "== MeetingCancellationRequest entity =="
cat -n manabom/src/main/java/mannabom_server/manabom/domain/meeting/entity/MeetingCancellationRequest.java | sed -n '1,130p'
echo "== MeetingCancellationStatus enum =="
cat -n manabom/src/main/java/mannabom_server/manabom/domain/meeting/enums/MeetingCancellationStatus.java | sed -n '1,120p'
echo "== SQL-only data model summary for meeting_cancellation_requests status values =="
python3 - <<'PY'
from pathlib import Path
for p in ['manabom/src/main/resources/db/migration/V21__add_meeting_cancellation_tables.sql',
'manabom/src/main/resources/db/migration/V23__remove_withdrawn_cancellation_status.sql']:
text = Path(p).read_text()
print(f'--- {p} ---')
for i, line in enumerate(text.splitlines(), 1):
if 'WITHDRAWN' in line or 'chk_cancellation_request_status' in line or p.endswith('V23') and any(x in line for x in ["ADD CONSTRAINT", "CHECK (status IN"]):
print(f'{i}: {line}')
PYRepository: mannabom/mannabomServer
Length of output: 8298
WITHDRAWN 기존 데이터를 먼저 정리해야 합니다.
V21에서 meeting_cancellation_requests.status = 'WITHDRAWN'이 허용되지만, V23은 해당 데이터를 유효 종료 상태로 변환하지 않고 바로 새 CHECK를 추가합니다. 기존에 WITHDRAWN 행이 있으면 마이그레이션이 실패하므로, 정책과 맞는 상태 변환 또는 사전 검증을 추가해 주세요.
🧰 Tools
🪛 Squawk (2.59.0)
[warning] 5-11: By default new constraints require a table scan and block writes to the table while that scan occurs. Use NOT VALID with a later VALIDATE CONSTRAINT call.
(constraint-missing-not-valid)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@manabom/src/main/resources/db/migration/V23__remove_withdrawn_cancellation_status.sql`
around lines 1 - 11, Update the V23 migration before recreating
chk_cancellation_request_status to handle existing WITHDRAWN rows: convert them
to the policy-approved terminal status or add the required pre-validation that
prevents the constraint from being applied with invalid data. Ensure the
migration succeeds when legacy WITHDRAWN records exist.
| CREATE UNIQUE INDEX uk_chat_members_active_room_user | ||
| ON chat_members (room_id, user_id) | ||
| WHERE status = 'ACTIVATE'; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n "executeInTransaction|mixed|flyway|Flyway" \
-g '*.yml' -g '*.yaml' -g '*.properties' -g '*.java' .Repository: mannabom/mannabomServer
Length of output: 282
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Flyway settings =="
for f in manabom/src/main/resources/application.yml manabom/src/main/resources/application-prod.yml; do
echo "-- $f"
sed -n '1,80p' "$f"
done
echo "== Migration file =="
sed -n '1,80p' manabom/src/main/resources/db/migration/V24__add_unique_active_chat_member.sql
echo "== ChatMemberStatus and Active indexes =="
rg -n "class ChatMemberStatus|enum ChatMemberStatus|CHAT_MEMBER|status = ['\"]ACTIVATE|uk_chat_members_active_room_user|CREATE UNIQUE INDEX" manabom -g '*.java' -g '*.sql' || trueRepository: mannabom/mannabomServer
Length of output: 4837
운영 중 쓰기 차단을 피할 인덱스 생성 방식을 사용해 주세요.
현재 Flyway 설정은 트랜잭션 실행 옵션을 지정하지 않아 기본값에 따라 마이그레이션이 트애잭션 안에서 실행될 수 있습니다. CREATE UNIQUE INDEX 대신 PostgreSQL의 CREATE UNIQUE INDEX CONCURRENTLY를 사용하고, 해당 마이그레이션이 트랜잭션 없이 실행되도록 spring.flyway.execute-in-transaction=false 설정을 추가해 운영 데이터가 클 때 배포 지연을 막아주세요.
🧰 Tools
🪛 Squawk (2.59.0)
[warning] 1-3: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.
(require-concurrent-index-creation)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@manabom/src/main/resources/db/migration/V24__add_unique_active_chat_member.sql`
around lines 1 - 3, Update the V24 migration to create the partial unique index
with PostgreSQL’s concurrent index creation syntax, and configure Flyway with
spring.flyway.execute-in-transaction=false so this migration runs outside a
transaction.
Source: Linters/SAST tools
작업 내용
Summary by CodeRabbit
새 기능
개선 사항