fix(member): 회원 차단을 요청 ID 단건만 적용한다
This commit is contained in:
37
docs/20260325_회원차단요청id만적용.md
Normal file
37
docs/20260325_회원차단요청id만적용.md
Normal file
@@ -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` → 성공
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user