feat: room_members 에 LEFT 상태 도입 (방 나가기/추방 시 row 유지)#134
Conversation
탈퇴 사용자의 SYSTEM 메시지 본문이 옛 닉네임으로 굳는 비일관 문제를 백엔드 데이터 변경 없이 클라이언트 재렌더링으로 해결한다는 결정을 ADR로 기록하고, MessagePayload와 MessageResponse의 content/metadata Schema 설명을 보강해 프론트가 metadata.eventType + userId 기반으로 본문을 조립한다는 계약을 Swagger/Springwolf 명세에 노출한다.
모든 마이그레이션이 TIMESTAMP WITH TIME ZONE으로 컬럼을 생성하는데 ERD와 user-withdrawal spec은 짧은 TIMESTAMP로 표기되어 있어 운영/개발 해석에 혼선이 생길 수 있었다. erd.md 16개 라인과 spec의 ALTER 예시를 실제 DDL과 1:1로 정렬한다.
호스트 본인이 PENDING 입장 승인과 본인 탈퇴를 동시에 발사하면 READ COMMITTED 스냅샷 차이로 솔로 호스트 방 판단 → HOST 멤버십만 삭제 → 호스트 없는 활성 방(orphan room)이 남는 시나리오가 가능했다. withdraw 트랜잭션 시작 시 본인이 HOST인 모든 Room을 PESSIMISTIC_WRITE로 잠그고, approve 트랜잭션 시작 시에도 Room을 findByIdForUpdate로 잡아 두 트랜잭션이 같은 Room 행을 두고 직렬화되도록 한다.
기존 users_active_required CHECK는 OR 구조라 탈퇴 회원(deleted_at IS NOT NULL)에 대한 4개 개인정보 컬럼(email, nickname, provider, provider_id)의 NULL 강제가 없어, User.anonymize() 누락이나 운영자의 수동 SQL로 익명화 안 된 탈퇴 행이 생기는 경로를 DB가 막아주지 못했다. CHECK를 양방향(iff) 구조로 교체해 "활성 ⇔ 4개 컬럼 모두 NOT NULL" / "탈퇴 ⇔ 4개 컬럼 모두 NULL"을 강제한다. 같은 V1.7의 기존 UNIQUE 제약 DROP은 환경별 이름 차이/핫픽스 잔재로 실패할 수 있어 DROP CONSTRAINT IF EXISTS로 방어한다. V1.7이 아직 main에 머지되지 않아 새 V1.8 대신 V1.7 자체를 수정한다. ERD/spec/결정 기록도 동일 정책으로 동기화.
📝 Walkthrough워크스루(Walkthrough)사용자 탈퇴를 soft delete로, 방 멤버 떠나기/추방을 상태 전이(LEFT)로 관리하는 라이프사이클 통합 변경. 정책 문서, DB 마이그레이션, 도메인 모델(MemberStatus enum, RoomMember 메서드), 리포지토리 쿼리, 서비스 계층(권한, 멤버 관리, 초대), API 응답 계약, 테스트를 포괄적으로 구현합니다. 변경 사항Room Member LEFT 상태 및 User 탈퇴 통합
예상 코드 리뷰 난이도🎯 4 (Complex) | ⏱️ ~60 minutes 시
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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: 1
🧹 Nitpick comments (2)
src/main/java/com/howaboutus/backend/rooms/repository/RoomRepository.java (1)
24-34: ⚡ Quick win잠금 대상은 ACTIVE HOST로 한정하는 게 안전합니다.
Line 30 조건에
m.status = ACTIVE가 없어 비활성/이상 데이터까지 잠글 수 있습니다. 불필요한 잠금 경쟁을 줄이려면 ACTIVE 필터를 같이 두는 편이 좋습니다.🔧 제안 수정
where r.id in ( select m.room.id from RoomMember m where m.user.id = :userId and m.role = com.howaboutus.backend.rooms.entity.RoomRole.HOST + and m.status = com.howaboutus.backend.rooms.entity.MemberStatus.ACTIVE )🤖 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 `@src/main/java/com/howaboutus/backend/rooms/repository/RoomRepository.java` around lines 24 - 34, The query in lockHostRoomsByUser currently locks rooms for any host membership state; restrict the lock to only active host memberships by adding an ACTIVE status predicate to the inner where clause (e.g., add "and m.status = com.howaboutus.backend.rooms.entity.RoomMemberStatus.ACTIVE" alongside "m.role = com.howaboutus.backend.rooms.entity.RoomRole.HOST") so the method lockHostRoomsByUser only locks rooms where the RoomMember m is both HOST and ACTIVE.docs/ai/features.md (1)
57-57: ⚡ Quick win메인 기능 명세는 짧게 유지하고 상세 규칙은 분리 문서로 링크해 주세요.
Line 57, Line 83~85는 구현 상세가 과도하게 길어져 표 가독성이 떨어집니다. 기능 표에는 핵심 요약만 두고, 상세 정책(LEFT 노출/online 강제/탈퇴 표시 규칙)은
docs/ai/decisions/*또는 별도docs/ai/*문서로 분리해 참조하는 편이 좋습니다.As per coding guidelines, "**/*.md: Split detailed explanations into separate documents under
docs/ai/, keeping this main file brief with only 'when to read which document' references".✂️ 예시 축약안
-| `[x]` | 회원 탈퇴 | `DELETE /users/me`. users는 soft delete + 익명화(email/nickname/profile/provider/provider_id NULL, deleted_at 설정), room_members는 ACTIVE/LEFT 무관하게 hard delete. HOST 방은 사전 위임 필요(422 + roomsRequiringDelegation), 1인 HOST 방은 자동 hard delete. Redis RTK는 AFTER_COMMIT에서 일괄 폐기. 채팅/북마크/일정의 BIGINT 작성자 ID는 유지. 클라이언트는 `GET /rooms/{id}/members` 응답(ACTIVE + LEFT 모두 포함)에서 닉네임/프로필을 조회하고, 그래도 members에 없는 ID는 **회원 탈퇴자**이며 "(알 수 없음)"으로 표시 | users, room_members, Redis | +| `[x]` | 회원 탈퇴 | `DELETE /users/me`. users soft delete+익명화, room_members hard delete. 작성자 표시/멤버 매핑 상세 규칙은 `docs/ai/decisions/20260607-user-withdrawal-soft-delete.md` 참조 | users, room_members, Redis | -| `[x]` | 방 멤버 목록 조회 | 방 참여자 목록 + 역할(HOST/MEMBER) + 상태(ACTIVE/LEFT) + 접속 상태. LEFT 멤버는 닉네임/프로필 조회용으로 함께 반환되며 `online`은 항상 false. AI 컨텍스트에서는 LEFT 멤버 제외 | room_members | +| `[x]` | 방 멤버 목록 조회 | 역할/상태/접속 상태를 조회(LEFT 포함, LEFT online=false). 상세 계약은 관련 결정 문서 참조 | room_members |Also applies to: 83-85
🤖 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 `@docs/ai/features.md` at line 57, The table row for "회원 탈퇴" is too detailed and harms readability; extract the implementation-specific rules (soft-delete/익명화 details, room_members hard delete rules, HOST delegation/422 behavior, Redis AFTER_COMMIT eviction, LEFT/ACTIVE exposure and withdrawn-member display rules) into a separate decision doc (e.g., a member-deletion policy under the AI docs) and replace the cell in docs/ai/features.md with a short summary mentioning only the key action and endpoints ("회원 탈퇴 — DELETE /users/me", "GET /rooms/{id}/members") plus a single-line pointer to the new policy; retain the important identifiers (users, room_members, Redis, GET /rooms/{id}/members, DELETE /users/me) so reviewers can find the full rules in the new document.Source: Coding guidelines
🤖 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 `@src/main/java/com/howaboutus/backend/rooms/entity/RoomMember.java`:
- Around line 121-135: The leave() and kicked() transitions in RoomMember
currently only check status and can set members with role == PENDING to LEFT,
violating the DB constraint ck_room_members_left_role; update the domain to
prevent PENDING→LEFT by adding a guard in RoomMember.leave() and
RoomMember.kicked() that throws or returns if this.role == MemberRole.PENDING,
or alternatively implement a separate cancel/decline transition (e.g.,
cancelPending()) that handles PENDING members without setting status to LEFT and
sets an appropriate status/timestamp, ensuring the domain invariant matches the
DB constraint (refer to RoomMember.status, RoomMember.role, MemberStatus and the
ck_room_members_left_role constraint).
---
Nitpick comments:
In `@docs/ai/features.md`:
- Line 57: The table row for "회원 탈퇴" is too detailed and harms readability;
extract the implementation-specific rules (soft-delete/익명화 details, room_members
hard delete rules, HOST delegation/422 behavior, Redis AFTER_COMMIT eviction,
LEFT/ACTIVE exposure and withdrawn-member display rules) into a separate
decision doc (e.g., a member-deletion policy under the AI docs) and replace the
cell in docs/ai/features.md with a short summary mentioning only the key action
and endpoints ("회원 탈퇴 — DELETE /users/me", "GET /rooms/{id}/members") plus a
single-line pointer to the new policy; retain the important identifiers (users,
room_members, Redis, GET /rooms/{id}/members, DELETE /users/me) so reviewers can
find the full rules in the new document.
In `@src/main/java/com/howaboutus/backend/rooms/repository/RoomRepository.java`:
- Around line 24-34: The query in lockHostRoomsByUser currently locks rooms for
any host membership state; restrict the lock to only active host memberships by
adding an ACTIVE status predicate to the inner where clause (e.g., add "and
m.status = com.howaboutus.backend.rooms.entity.RoomMemberStatus.ACTIVE"
alongside "m.role = com.howaboutus.backend.rooms.entity.RoomRole.HOST") so the
method lockHostRoomsByUser only locks rooms where the RoomMember m is both HOST
and ACTIVE.
🪄 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 Plus
Run ID: 3356d00b-cc8f-4ed3-8510-e6ec9a8b7e80
📒 Files selected for processing (29)
docs/ai/decisions/20260607-1636-system-message-metadata-rendering.mddocs/ai/decisions/20260607-user-withdrawal-soft-delete.mddocs/ai/erd.mddocs/ai/features.mddocs/superpowers/plans/2026-06-08-room-member-left-status.mddocs/superpowers/specs/2026-06-07-user-withdrawal-design.mddocs/superpowers/specs/2026-06-08-room-member-left-status-design.mdsrc/main/java/com/howaboutus/backend/ai/repository/AiContextQueryRepositoryImpl.javasrc/main/java/com/howaboutus/backend/messages/controller/dto/MessageResponse.javasrc/main/java/com/howaboutus/backend/realtime/service/dto/MessagePayload.javasrc/main/java/com/howaboutus/backend/rooms/controller/dto/RoomMemberResponse.javasrc/main/java/com/howaboutus/backend/rooms/entity/MemberStatus.javasrc/main/java/com/howaboutus/backend/rooms/entity/RoomMember.javasrc/main/java/com/howaboutus/backend/rooms/repository/RoomMemberRepository.javasrc/main/java/com/howaboutus/backend/rooms/repository/RoomRepository.javasrc/main/java/com/howaboutus/backend/rooms/service/RoomAuthorizationService.javasrc/main/java/com/howaboutus/backend/rooms/service/RoomInviteService.javasrc/main/java/com/howaboutus/backend/rooms/service/RoomMemberService.javasrc/main/java/com/howaboutus/backend/rooms/service/dto/RoomMemberResult.javasrc/main/java/com/howaboutus/backend/user/service/UserWithdrawalService.javasrc/main/resources/db/migration/V1.7__users_withdrawal.sqlsrc/main/resources/db/migration/V1.8__room_members_status.sqlsrc/test/java/com/howaboutus/backend/rooms/controller/RoomControllerTest.javasrc/test/java/com/howaboutus/backend/rooms/entity/RoomMemberTest.javasrc/test/java/com/howaboutus/backend/rooms/repository/RoomMemberStatusConstraintTest.javasrc/test/java/com/howaboutus/backend/rooms/service/RoomAuthorizationServiceTest.javasrc/test/java/com/howaboutus/backend/rooms/service/RoomInviteServiceTest.javasrc/test/java/com/howaboutus/backend/rooms/service/RoomMemberServiceTest.javasrc/test/java/com/howaboutus/backend/user/service/UserWithdrawalServiceTest.java



변경 내용
room_members에status (ACTIVE/LEFT)+left_at컬럼 도입 (V1.8 마이그레이션, CHECK 제약 3종 + 인덱스 추가)leave) / 추방(kick)을 hard delete →status=LEFT전환으로 변경requireActiveMember가드 강화:status=ACTIVE만 멤버로 인정 (LEFT는 비멤버 차단)status=ACTIVE필터 일괄 적용requestJoin이 LEFT row 를 PENDING 으로 UPSERT (재입장 지원)GET /rooms/{id}/members응답이 LEFT 멤버까지 노출 +online=false강제 (과거 메시지 닉네임/프로필 렌더링용)RoomMemberResponse에status필드 노출DROP CONSTRAINT방어 포함)TIMESTAMP WITH TIME ZONE으로 통일docs/ai/erd.md,docs/ai/features.md갱신 + 설계 문서(docs/superpowers/specs|plans) 추가변경 이유
room_membersrow 를 hard delete 하여, 클라이언트가 과거 메시지의 작성자 닉네임/프로필을 표시할 수 없었음.status=LEFT도입.GET /rooms/{id}/members호출로 해결.테스트
./gradlew build(Checkstyle 포함)/review-code-against-docs스킬로 검증 (구조/컨벤션/스펙/ERD 정합성)RoomMember도메인 메서드(leave/kicked/rejoinAsPending) 단위 테스트RoomAuthorizationServiceLEFT 차단 단위 테스트RoomInviteServiceLEFT → PENDING 부활 단위 테스트RoomMemberServiceleave/kick LEFT 전환 + 중복 이벤트 차단 단위 테스트UserWithdrawalServiceLEFT 멤버십 hard delete 통합 테스트체크리스트
docs/ai/erd.md,docs/ai/features.md,docs/ai/decisions/*,docs/superpowers/*)하네스 변경 체크리스트
Summary by CodeRabbit
Release Notes
New Features
Documentation
Refactor