diff --git a/docs/20260325_회원차단요청id만적용.md b/docs/20260325_회원차단요청id만적용.md new file mode 100644 index 00000000..7db8ab29 --- /dev/null +++ b/docs/20260325_회원차단요청id만적용.md @@ -0,0 +1,37 @@ +# 20260325 회원 차단 요청 id만 적용 + +- [x] memberBlock 호출 흐름 및 동일 auth 일괄 차단 지점 확인 +- [x] memberBlock 로직을 request.id 단일 차단으로 수정 +- [x] 관련 테스트 보강 및 회귀 검증 +- [x] LSP 진단, 테스트, 빌드 검증 수행 + +## 2차 수정 체크리스트 + +- [x] `MemberService.memberBlock` 의미 단위 주석 추가 +- [x] `MemberServiceCacheEvictionTest` 신규 테스트 의미 단위 주석 추가 +- [x] 테스트 및 빌드 재검증 + +## 검증 기록 + +### 1차 구현 +- 무엇을: `MemberService.memberBlock`에서 동일 `auth` 기반 다중 계정 확장 차단을 제거하고, `request.blockMemberId` 1건만 차단/재활성화하도록 수정했다. +- 왜: 회원 차단 API가 요청한 대상 ID만 차단해야 하며, 동일 auth 계정 전체가 함께 차단되는 과차단 동작을 제거해야 하기 때문이다. +- 어떻게: + - 탐색: explore 2개 + librarian 1개 백그라운드 분석, `grep`/`ast-grep`/`glob`로 호출 흐름과 확장 지점 확인. + - 코드 변경: `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt`의 `memberBlock`에서 `authRepository.getMemberIdsByNameAndBirthAndDiAndGender(...)` 및 다중 루프 제거. + - 테스트 변경: `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceCacheEvictionTest.kt`에 `shouldBlockOnlyRequestedMemberEvenWhenTargetHasAuth` 추가. + - 진단: `lsp_diagnostics` 실행 시 `.kt` LSP 서버 미구성으로 진단 불가 확인. + - 검증 명령: + - `./gradlew test --tests "kr.co.vividnext.sodalive.member.MemberServiceCacheEvictionTest"` → 성공 + - `./gradlew build` → 성공 + +### 2차 수정 +- 무엇을: 1차에서 작성한 `memberBlock` 변경 코드와 회귀 테스트 코드에 의미 단위 주석을 추가했다. +- 왜: 요청하신 대로 작성된 코드의 의도를 블록 단위로 바로 파악할 수 있도록 하기 위해서다. +- 어떻게: + - 코드 변경: `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt`의 `memberBlock`에 검증/단일대상차단/캐시무효화 의도 주석 추가. + - 코드 변경: `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceCacheEvictionTest.kt`의 `shouldBlockOnlyRequestedMemberEvenWhenTargetHasAuth`에 준비/실행/검증 주석 추가. + - 진단: `lsp_diagnostics` 실행 시 `.kt` LSP 서버 미구성으로 진단 불가 확인. + - 검증 명령: + - `./gradlew test --tests "kr.co.vividnext.sodalive.member.MemberServiceCacheEvictionTest"` → 성공 + - `./gradlew build` → 성공 diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 4b34411e..254c0e59 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -528,35 +528,25 @@ class MemberService( @Transactional fun memberBlock(request: MemberBlockRequest, memberId: Long) { + // 요청자와 차단 대상 회원이 실제로 존재하는지 검증한다. val member = repository.findByIdOrNull(id = memberId) ?: throw SodaException(messageKey = "common.error.invalid_request") val blockedMember = repository.findByIdOrNull(id = request.blockMemberId) ?: throw SodaException(messageKey = "common.error.invalid_request") - val blockTargetMemberIds = mutableSetOf(request.blockMemberId) - blockedMember.auth?.let { auth -> - val verifiedMemberIds = authRepository.getMemberIdsByNameAndBirthAndDiAndGender( - name = auth.name, - birth = auth.birth, - di = auth.di, - gender = auth.gender - ) - blockTargetMemberIds.addAll(verifiedMemberIds) - } - blockTargetMemberIds.remove(memberId) - - blockTargetMemberIds.forEach { targetMemberId -> - val targetMember = repository.findByIdOrNull(id = targetMemberId) ?: return@forEach - + // 요청자 본인을 차단하려는 경우에는 차단 레코드를 생성하지 않는다. + if (memberId != request.blockMemberId) { + // 요청한 blockMemberId 한 건만 대상으로 기존 차단 여부를 조회한다. var blockMember = blockMemberRepository.getBlockAccount( - blockedMemberId = targetMemberId, + blockedMemberId = request.blockMemberId, memberId = memberId ) + // 기존 레코드가 없으면 생성하고, 있으면 활성 상태로 전환한다. if (blockMember == null) { blockMember = BlockMember() blockMember.member = member - blockMember.blockedMember = targetMember + blockMember.blockedMember = blockedMember blockMemberRepository.save(blockMember) } else { @@ -564,11 +554,14 @@ class MemberService( } } + // 차단 반영 후 요청자 기준 캐시를 즉시 무효화한다. evictRecommendLiveCache(memberId) evictLatestFinishedLiveCache(memberId) - blockTargetMemberIds.forEach { - evictRecommendLiveCache(it) - evictLatestFinishedLiveCache(it) + + // 본인 차단이 아닌 경우 요청한 대상 회원의 캐시도 함께 무효화한다. + if (memberId != request.blockMemberId) { + evictRecommendLiveCache(request.blockMemberId) + evictLatestFinishedLiveCache(request.blockMemberId) } } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceCacheEvictionTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceCacheEvictionTest.kt index 134ca3d3..2b8ee297 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceCacheEvictionTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceCacheEvictionTest.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.common.CountryContext import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.SodaMessageSource +import kr.co.vividnext.sodalive.member.auth.Auth import kr.co.vividnext.sodalive.member.auth.AuthRepository import kr.co.vividnext.sodalive.member.block.BlockMember import kr.co.vividnext.sodalive.member.block.BlockMemberRepository @@ -92,6 +93,59 @@ class MemberServiceCacheEvictionTest { Mockito.verifyNoInteractions(authRepository) } + @Test + fun shouldBlockOnlyRequestedMemberEvenWhenTargetHasAuth() { + // 차단 대상에게 본인인증 정보가 연결된 상황을 준비한다. + val memberId = 500L + val blockedMemberId = 600L + val linkedMemberId = 601L + val member = createMember(id = memberId, nickname = "requester2") + val blockedMember = createMember(id = blockedMemberId, nickname = "target2") + val auth = Auth( + name = "홍길동", + birth = "19900101", + uniqueCi = "unique-ci", + di = "di-value", + gender = 1 + ) + auth.member = blockedMember + + // 요청자와 요청 대상만 조회 가능하도록 목 동작을 설정한다. + Mockito.`when`(memberRepository.findById(memberId)).thenReturn(Optional.of(member)) + Mockito.`when`(memberRepository.findById(blockedMemberId)).thenReturn(Optional.of(blockedMember)) + Mockito.`when`( + blockMemberRepository.getBlockAccount( + blockedMemberId = blockedMemberId, + memberId = memberId + ) + ).thenReturn(null) + Mockito.`when`( + authRepository.getMemberIdsByNameAndBirthAndDiAndGender( + name = auth.name, + birth = auth.birth, + di = auth.di, + gender = auth.gender + ) + ).thenReturn(listOf(blockedMemberId, linkedMemberId)) + + // 차단 API를 실행한다. + service.memberBlock(MemberBlockRequest(blockMemberId = blockedMemberId), memberId) + + // 요청한 blockMemberId 한 건만 차단 처리 및 캐시 무효화되는지 검증한다. + Mockito.verify(blockMemberRepository).getBlockAccount( + blockedMemberId = blockedMemberId, + memberId = memberId + ) + Mockito.verify(blockMemberRepository, Mockito.never()).getBlockAccount( + blockedMemberId = linkedMemberId, + memberId = memberId + ) + Mockito.verify(cache).evict("getRecommendLive:$memberId") + Mockito.verify(cache).evict("getRecommendLive:$blockedMemberId") + Mockito.verify(cache, Mockito.never()).evict("getRecommendLive:$linkedMemberId") + Mockito.verifyNoInteractions(authRepository) + } + @Test fun shouldEvictRecommendLiveCacheForRequesterAndTargetOnUnblock() { val memberId = 300L