feat(following): 팔로우 해제 확인 dialog를 적용한다

This commit is contained in:
2026-08-18 11:57:21 +09:00
parent 16633b978e
commit 388304dbaa
13 changed files with 234 additions and 101 deletions
@@ -11,8 +11,8 @@ import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityFollowingCreatorBinding
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
import org.koin.android.ext.android.inject
@@ -43,24 +43,22 @@ class FollowingCreatorActivity : BaseActivity<ActivityFollowingCreatorBinding>(
)
},
onClickFollow = { creatorId, isFollow ->
if (isFollow) {
val notifyFragment = CreatorFollowNotifyFragment(
onClickNotifyAll = {
viewModel.follow(creatorId, follow = true, notify = true)
},
onClickNotifyNone = {
viewModel.follow(creatorId, follow = true, notify = false)
},
onClickUnFollow = {
viewModel.follow(creatorId, follow = false, notify = false)
}
)
if (notifyFragment.isAdded) return@FollowingCreatorAdapter
notifyFragment.show(supportFragmentManager, notifyFragment.tag)
} else {
if (!isFollow) {
viewModel.follow(creatorId)
}
},
onClickUnfollow = { creatorId, creatorName ->
V2ModalDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(R.string.creator_unfollow_confirm_title),
desc = getString(R.string.creator_unfollow_confirm_desc, creatorName),
confirmButtonTitle = getString(R.string.creator_unfollow_confirm_confirm),
confirmButtonClick = {
viewModel.follow(creatorId, follow = false, notify = false)
},
cancelButtonTitle = getString(R.string.creator_unfollow_confirm_cancel)
).show(screenWidth)
}
)
@@ -9,10 +9,12 @@ import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemFollowerListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class FollowingCreatorAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickFollow: (Long, Boolean) -> Unit
private val onClickFollow: (Long, Boolean) -> Unit,
private val onClickUnfollow: ((Long, String) -> Unit)? = null
) : RecyclerView.Adapter<FollowingCreatorAdapter.ViewHolder>() {
private val items = mutableListOf<GetCreatorFollowingAllListItem>()
@@ -31,19 +33,22 @@ class FollowingCreatorAdapter(
binding.ivFollow.visibility = View.VISIBLE
if (item.isFollow) {
binding.ivFollow.setImageResource(
if (item.isNotify) {
R.drawable.btn_following_big
} else {
R.drawable.btn_following_no_alarm_big
}
)
val padding = 8.dpToPx().toInt()
binding.ivFollow.setPadding(padding, padding, padding, padding)
binding.ivFollow.setBackgroundResource(R.drawable.bg_following_circle_gray_800)
binding.ivFollow.setImageResource(R.drawable.ic_new_following_white)
} else {
binding.ivFollow.setPadding(0, 0, 0, 0)
binding.ivFollow.background = null
binding.ivFollow.setImageResource(R.drawable.btn_follow_big)
}
binding.ivFollow.setOnClickListener {
onClickFollow(item.creatorId, item.isFollow)
if (item.isFollow && onClickUnfollow != null) {
onClickUnfollow.invoke(item.creatorId, item.nickname)
} else {
onClickFollow(item.creatorId, item.isFollow)
}
}
binding.root.setOnClickListener { onClickItem(item.creatorId) }
}
@@ -332,7 +332,17 @@ class CreatorChannelActivity :
return
}
homeActionDelegate?.follow(follow = false, notify = false)
V2ModalDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(R.string.creator_unfollow_confirm_title),
desc = getString(R.string.creator_unfollow_confirm_desc, header.nickname),
confirmButtonTitle = getString(R.string.creator_unfollow_confirm_confirm),
confirmButtonClick = {
homeActionDelegate?.follow(follow = false, notify = false)
},
cancelButtonTitle = getString(R.string.creator_unfollow_confirm_cancel)
).show(screenWidth)
}
private fun onBellClicked() {
Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/gray_800" />
<corners android:radius="100dp" />
</shape>
+4
View File
@@ -345,6 +345,10 @@
<string name="creator_channel_dm_check_button">Check DMs</string>
<string name="creator_channel_follow_button">Follow</string>
<string name="creator_channel_unfollow_success">Following has been canceled.</string>
<string name="creator_unfollow_confirm_title">Unfollow</string>
<string name="creator_unfollow_confirm_desc">Youll no longer receive updates from %1$s.\nDo you want to unfollow?</string>
<string name="creator_unfollow_confirm_cancel">Cancel</string>
<string name="creator_unfollow_confirm_confirm">Unfollow</string>
<string name="creator_channel_block_success">Blocked.</string>
<string name="creator_channel_section_live">Live now</string>
<string name="creator_channel_section_latest_audio">Latest audio</string>
+4
View File
@@ -345,6 +345,10 @@
<string name="creator_channel_dm_check_button">DMを確認</string>
<string name="creator_channel_follow_button">フォロー</string>
<string name="creator_channel_unfollow_success">フォローを解除しました。</string>
<string name="creator_unfollow_confirm_title">フォロー解除</string>
<string name="creator_unfollow_confirm_desc">%1$sからの最新情報を受け取れなくなります。\nフォローを解除しますか?</string>
<string name="creator_unfollow_confirm_cancel">キャンセル</string>
<string name="creator_unfollow_confirm_confirm">解除する</string>
<string name="creator_channel_block_success">ブロックしました。</string>
<string name="creator_channel_section_live">配信中</string>
<string name="creator_channel_section_latest_audio">最新オーディオ</string>
+4
View File
@@ -344,6 +344,10 @@
<string name="creator_channel_dm_check_button">DM 확인하기</string>
<string name="creator_channel_follow_button">팔로우</string>
<string name="creator_channel_unfollow_success">팔로잉이 취소되었습니다.</string>
<string name="creator_unfollow_confirm_title">팔로우 해제</string>
<string name="creator_unfollow_confirm_desc">팔로우를 해제할까요?\n%1$s의 소식을 받을 수 없어요.</string>
<string name="creator_unfollow_confirm_cancel">취소</string>
<string name="creator_unfollow_confirm_confirm">해제하기</string>
<string name="creator_channel_block_success">차단하였습니다.</string>
<string name="creator_channel_section_live">현재 라이브</string>
<string name="creator_channel_section_latest_audio">최신 오디오</string>
@@ -0,0 +1,50 @@
package kr.co.vividnext.sodalive.following
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
class FollowingCreatorActivitySourceTest {
@Test
fun `팔로잉 리스트는 Bottom Sheet 대신 해제 확인 Dialog를 표시한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivity.kt"
).readText()
assertFalse(source.contains("CreatorFollowNotifyFragment"))
assertTrue(source.contains("onClickUnfollow = { creatorId, creatorName ->"))
assertTrue(source.contains("V2ModalDialog("))
assertTrue(source.contains("R.string.creator_unfollow_confirm_desc, creatorName"))
assertTrue(source.contains("confirmButtonClick = {"))
assertTrue(source.contains("viewModel.follow(creatorId, follow = false, notify = false)"))
}
@Test
fun `팔로우 해제 확인 문구는 세 locale에서 같은 placeholder 계약을 사용한다`() {
val korean = projectFile("app/src/main/res/values/strings.xml").readText()
val english = projectFile("app/src/main/res/values-en/strings.xml").readText()
val japanese = projectFile("app/src/main/res/values-ja/strings.xml").readText()
listOf(korean, english, japanese).forEach { strings ->
assertTrue(strings.contains("name=\"creator_unfollow_confirm_title\""))
assertTrue(strings.contains("name=\"creator_unfollow_confirm_desc\""))
assertTrue(strings.contains("name=\"creator_unfollow_confirm_cancel\""))
assertTrue(strings.contains("name=\"creator_unfollow_confirm_confirm\""))
assertTrue(strings.contains("%1\$s"))
}
assertTrue(
english.contains(
"Youll no longer receive updates from %1\$s.\\nDo you want to unfollow?"
)
)
}
private fun projectFile(path: String): File = File(projectRoot(), path)
private fun projectRoot(): File {
return generateSequence(File(System.getProperty("user.dir") ?: ".").absoluteFile) { it.parentFile }
.first { File(it, "settings.gradle").exists() }
}
}
@@ -0,0 +1,32 @@
package kr.co.vividnext.sodalive.following
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
class FollowingCreatorAdapterSourceTest {
@Test
fun `팔로잉 중 버튼은 흰색 아이콘과 gray 800 원형 배경을 사용한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/following/FollowingCreatorAdapter.kt"
).readText()
val background = projectFile(
"app/src/main/res/drawable/bg_following_circle_gray_800.xml"
)
assertTrue(source.contains("R.drawable.ic_new_following_white"))
assertTrue(source.contains("R.drawable.bg_following_circle_gray_800"))
assertTrue(source.contains("8.dpToPx().toInt()"))
assertTrue(background.exists())
assertTrue(background.readText().contains("@color/gray_800"))
assertTrue(background.readText().contains("android:radius=\"100dp\""))
}
private fun projectFile(path: String): File = File(projectRoot(), path)
private fun projectRoot(): File {
return generateSequence(File(System.getProperty("user.dir") ?: ".").absoluteFile) { it.parentFile }
.first { File(it, "settings.gradle").exists() }
}
}
@@ -283,6 +283,11 @@ class CreatorChannelActivitySourceTest {
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
val followClickSource = sourceSection(
source = source,
startMarker = "private fun onFollowCapsuleClicked()",
endMarker = "private fun onBellClicked()"
)
assertFalse(source.contains("CreatorFollowNotifyFragment"))
assertTrue(layout.contains("android:id=\"@+id/layout_follow_capsule\""))
@@ -294,7 +299,11 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("if (!header.isFollow)"))
assertTrue(source.contains("homeActionDelegate?.follow(follow = true, notify = true)"))
assertTrue(source.contains("homeActionDelegate?.follow(follow = true, notify = !header.isNotify)"))
assertTrue(source.contains("homeActionDelegate?.follow(follow = false, notify = false)"))
assertTrue(followClickSource.contains("V2ModalDialog("))
assertTrue(followClickSource.contains("R.string.creator_unfollow_confirm_title"))
assertTrue(followClickSource.contains("R.string.creator_unfollow_confirm_desc, header.nickname"))
assertTrue(followClickSource.contains("confirmButtonClick = {"))
assertTrue(followClickSource.contains("homeActionDelegate?.follow(follow = false, notify = false)"))
assertFalse(source.contains("showFollowNotifyFragment"))
assertTrue(source.contains("onCreatorChannelFollowProgressChanged"))
assertTrue(source.contains("binding.layoutFollowCapsule.isEnabled = titleBarState.isActionEnabled"))
@@ -1317,7 +1326,12 @@ class CreatorChannelActivitySourceTest {
assertTrue(en.contains("name=\"creator_channel_home_donation_empty_title\">Be the first to\\nsupport this creator!"))
assertTrue(ja.contains("name=\"creator_channel_home_donation_empty_title\">最初のサポートをしてみましょう!"))
assertTrue(ko.contains("name=\"creator_channel_donation_empty_title\">아직 후원이 없습니다.\\n처음으로 크리에이터를 후원해 보세요!"))
assertTrue(en.contains("name=\"creator_channel_donation_empty_title\">No support yet.\\nBe the first to support this creator!"))
assertTrue(
en.contains(
"name=\"creator_channel_donation_empty_title\">No support yet.\\n" +
"Be the first to support this creator!"
)
)
assertTrue(ja.contains("name=\"creator_channel_donation_empty_title\">まだサポートがありません。\\n最初のサポートをしてみましょう!"))
assertTrue(ko.contains("name=\"creator_channel_donation_action\">후원하기"))
assertTrue(en.contains("name=\"creator_channel_donation_action\">Support"))
@@ -12,11 +12,11 @@
| 문서 항목 | 내용 |
|---|---|
| 상태 | 계획 작성 완료 |
| 상태 | 구현 완료 |
| 작성일 | `2026-08-18` |
| 요구사항 기준 | `docs/20260818_팔로잉_해제_확인과_버튼_UI/prd.md` |
| API 기준 | 변경 없음 |
| 현재 Phase | 구현 대기 |
| 현재 Phase | 완료 |
| 현재 활성 Goal | 없음 |
## 목표
@@ -27,11 +27,11 @@
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 대기 | `0/3` | `P1-T1` | 코드 구현 승인 필요 |
| 2 | 대기 | `0/1` | `P2-T1` | Phase 1 완료 필요 |
| 3 | 대기 | `0/2` | `P3-T1` | Phase 2 완료 필요 |
| 4 | 대기 | `0/1` | `P4-T1` | Phase 2 완료 필요 |
| 5 | 대기 | `0/1` | `P5-GATE` | Phase 1~4 완료 필요 |
| 1 | 완료 | `3/3` | 없음 | 없음 |
| 2 | 완료 | `1/1` | 없음 | 없음 |
| 3 | 완료 | `2/2` | 없음 | 없음 |
| 4 | 완료 | `1/1` | 없음 | 없음 |
| 5 | 완료 | `1/1` | 없음 | 없음 |
## 범위
@@ -103,11 +103,11 @@
- Modify: equivalent existing following adapter source/local test if present
- Test: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorAdapterSourceTest.kt`
- [ ] **RED:** `FollowingCreatorAdapter.kt`가 팔로잉 중 branch에서 `R.drawable.ic_new_following_white``R.drawable.bg_creator_channel_following_capsule` 또는 동등한 `gray_800` 원형 배경을 참조하는지 검증하는 source/local test를 작성한다.
- [ ] **RED 확인:** `./gradlew :app:test --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"`를 실행해 현재 팔로잉 중 branch가 기존 이미지 리소스 사용하므로 실패함을 확인다.
- [ ] **GREEN:** 이 Task에서 production 구현을 하지 않는다. 다음 구현 Task `P3-T1`에서 처리한다.
- [ ] **GREEN 확인:** `P3-T1` 완료 후 같은 focused test 성공을 기록한다.
- [ ] **REFACTOR:** test가 문자열 과검증이면 요청 리소스 계약만 남기고 이번 Task가 만든 중복만 정리한다.
- [x] **RED:** `FollowingCreatorAdapter.kt`가 팔로잉 중 branch에서 `R.drawable.ic_new_following_white``R.drawable.bg_following_circle_gray_800`을 참조하는지 검증하는 source/local test를 작성한다.
- [x] **RED 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"`를 실행해 기존 이미지 리소스 사용 때문에 실패함을 확인다.
- [x] **GREEN:** `P3-T1`에서 production 구현을 완료했다.
- [x] **GREEN 확인:** 같은 focused test 성공을 확인했다.
- [x] **REFACTOR:** 요청 리소스 계약만 남기고 과도한 검증은 추가하지 않았다.
#### Task 1.2 팔로잉 리스트 팔로우 해제 route 계약
@@ -123,12 +123,12 @@
- Modify: equivalent existing following activity source/local test if present
- Test: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivitySourceTest.kt`
- [ ] **RED:** `FollowingCreatorActivity.kt`가 팔로잉 중 branch에서 `CreatorFollowNotifyFragment`를 생성하지 않고 `V2ModalDialog`를 생성하는지 검증다.
- [ ] **RED:** 확인 callback 안에서만 `viewModel.follow(creatorId, follow = false, notify = false)`가 호출되는지 source/local test로 검증다.
- [ ] **RED 확인:** `./gradlew :app:test --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"`를 실행해 현재 Bottom Sheet route 때문에 실패함을 확인다.
- [ ] **GREEN:** 이 Task에서 production 구현을 하지 않는다. 다음 구현 Task `P3-T2`에서 처리한다.
- [ ] **GREEN 확인:** `P3-T2` 완료 후 같은 focused test 성공을 기록한다.
- [ ] **REFACTOR:** Dialog route와 unfollow callback 검증 외의 문자열 과검증은 제거한다.
- [x] **RED:** `FollowingCreatorActivity.kt`가 팔로잉 중 branch에서 `CreatorFollowNotifyFragment`를 생성하지 않고 `V2ModalDialog`를 생성하는지 검증다.
- [x] **RED:** 확인 callback 안에서만 `viewModel.follow(creatorId, follow = false, notify = false)`가 호출되는지 source/local test로 검증다.
- [x] **RED 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"`를 실행해 현재 Bottom Sheet route 때문에 실패함을 확인다.
- [x] **GREEN:** `P3-T2`에서 production 구현을 완료했다.
- [x] **GREEN 확인:** 같은 focused test 성공을 확인했다.
- [x] **REFACTOR:** Dialog route와 unfollow callback 검증 외의 과도한 검증은 추가하지 않았다.
#### Task 1.3 크리에이터 채널 팔로우 해제 확인 계약
@@ -143,12 +143,12 @@
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- [ ] **RED:** `onFollowCapsuleClicked()`의 팔로잉 중 branch가 `V2ModalDialog`를 생성하고 description에 현재 header nickname을 전달하도록 기대값을 갱신다.
- [ ] **RED:** `homeActionDelegate?.follow(follow = false, notify = false)` 호출이 Dialog confirm callback 안에만 있어야 함을 검증다.
- [ ] **RED 확인:** `./gradlew :app:test --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`를 실행해 현재 즉시 unfollow 구현 때문에 실패함을 확인다.
- [ ] **GREEN:** 이 Task에서 production 구현을 하지 않는다. 다음 구현 Task `P4-T1`에서 처리한다.
- [ ] **GREEN 확인:** `P4-T1` 완료 후 같은 focused test 성공을 기록한다.
- [ ] **REFACTOR:** 기존 title bar, owner, bell 동작 test가 깨지지 않도록 변경 범위를 팔로잉 해제 branch로 제한다.
- [x] **RED:** `onFollowCapsuleClicked()`의 팔로잉 중 branch가 `V2ModalDialog`를 생성하고 description에 현재 header nickname을 전달하도록 기대값을 갱신다.
- [x] **RED:** `homeActionDelegate?.follow(follow = false, notify = false)` 호출이 Dialog confirm callback 안에만 있어야 함을 검증다.
- [x] **RED 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다"`를 실행해 즉시 unfollow 구현 때문에 실패함을 확인다.
- [x] **GREEN:** `P4-T1`에서 production 구현을 완료했다.
- [x] **GREEN 확인:** focused test와 전체 `CreatorChannelActivitySourceTest` 성공을 확인했다.
- [x] **REFACTOR:** 기존 title bar, owner, bell 동작 test가 깨지지 않도록 변경 범위를 팔로잉 해제 branch로 제한다.
## Phase 2. 공통 Dialog 문구 리소스 추가
@@ -175,13 +175,13 @@
- Modify: `app/src/main/res/values-ja/strings.xml`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivitySourceTest.kt` or equivalent resource/source test
- [ ] **RED:** 세 locale에 `creator_unfollow_confirm_title`, `creator_unfollow_confirm_desc`, `creator_unfollow_confirm_cancel`, `creator_unfollow_confirm_confirm`가 없어서 실패하는 source/local test를 작성다.
- [ ] **RED 확인:** 관련 focused test를 실행해 resource 누락 실패를 확인다.
- [ ] **GREEN:** 한국어 `팔로우 해제`, `팔로우를 해제할까요?\n%1$s의 소식을 받을 수 없어요.`, `취소`, `해제하기`를 추가다.
- [ ] **GREEN:** 영어 `Unfollow`, `Youll no longer receive updates from %1$s.\nDo you want to unfollow?`, `Cancel`, `Unfollow`를 추가다.
- [ ] **GREEN:** 일본어 `フォロー解除`, `%1$sからの最新情報を受け取れなくなります。\nフォローを解除しますか?`, `キャンセル`, `解除する`를 추가다.
- [ ] **GREEN 확인:** focused test를 다시 실행해 resource 계약 성공을 확인다.
- [ ] **REFACTOR:** 중복 literal이 Activity에 남지 않도록 resource 소비 Task에서만 참조하도록 계획을 유지한다.
- [x] **RED:** 세 locale에 `creator_unfollow_confirm_title`, `creator_unfollow_confirm_desc`, `creator_unfollow_confirm_cancel`, `creator_unfollow_confirm_confirm`가 없어서 실패하는 source/local test를 작성다.
- [x] **RED 확인:** `FollowingCreatorActivitySourceTest`에서 resource 누락 실패를 확인다.
- [x] **GREEN:** 한국어 `팔로우 해제`, `팔로우를 해제할까요?\n%1$s의 소식을 받을 수 없어요.`, `취소`, `해제하기`를 추가다.
- [x] **GREEN:** 영어 `Unfollow`, `Youll no longer receive updates from %1$s.\nDo you want to unfollow?`, `Cancel`, `Unfollow`를 추가다.
- [x] **GREEN:** 일본어 `フォロー解除`, `%1$sからの最新情報を受け取れなくなります。\nフォローを解除しますか?`, `キャンセル`, `解除する`를 추가다.
- [x] **GREEN 확인:** focused test를 다시 실행해 resource 계약 성공을 확인다.
- [x] **REFACTOR:** Activity에 resource 참조만 남겼다.
## Phase 3. 팔로잉 리스트 변경
@@ -208,12 +208,12 @@
- Create: `app/src/main/res/drawable/bg_following_circle_gray_800.xml`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorAdapterSourceTest.kt`
- [ ] **RED:** `P1-T1` test 실패 상태를 확인다.
- [ ] **RED 확인:** 실패 원인이 팔로잉 중 리소스 불일치임을 기록다.
- [ ] **GREEN:** `item.isFollow` branch에서 `binding.ivFollow.setImageResource(R.drawable.ic_new_following_white)`를 사용하도록 최소 변경다.
- [ ] **GREEN:** `ivFollow``gray_800``100dp` radius를 사용하는 `bg_following_circle_gray_800`을 적용하고, 팔로잉 중에만 `8dp` padding을 적용해 크리에이터 채널과 같은 `36dp` 원형 크기를 만든다.
- [ ] **GREEN 확인:** `./gradlew :app:test --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"` 성공을 확인다.
- [ ] **REFACTOR:** 알림 on/off 분기에서 더 이상 다른 팔로잉 중 이미지를 쓰지 않도록 중복 branch만 정리한다.
- [x] **RED:** `P1-T1` test 실패 상태를 확인다.
- [x] **RED 확인:** 실패 원인이 팔로잉 중 리소스 불일치임을 기록다.
- [x] **GREEN:** `item.isFollow` branch에서 `binding.ivFollow.setImageResource(R.drawable.ic_new_following_white)`를 사용하도록 최소 변경다.
- [x] **GREEN:** `ivFollow``gray_800``100dp` radius를 사용하는 `bg_following_circle_gray_800`을 적용하고, 팔로잉 중에만 `8dp` padding을 적용다.
- [x] **GREEN 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"` 성공을 확인다.
- [x] **REFACTOR:** 알림 on/off 분기의 팔로잉 중 이미지 중복을 제거했다.
#### Task 3.2 팔로잉 리스트 Bottom Sheet route 제거와 확인 Dialog 적용
@@ -228,13 +228,13 @@
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivity.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/following/FollowingCreatorActivitySourceTest.kt`
- [ ] **RED:** `P1-T2` test 실패 상태를 확인다.
- [ ] **RED 확인:** 실패 원인이 `CreatorFollowNotifyFragment` route와 Dialog 미사용임을 기록다.
- [ ] **GREEN:** `onClickFollow` callback이 Dialog description을 만들 수 있도록 클릭 대상 creator name을 받을 수 있는 최소 경로를 정다.
- [ ] **GREEN:** 팔로잉 중 branch에서 `CreatorFollowNotifyFragment` 생성과 `show()` 호출을 제거다.
- [ ] **GREEN:** 팔로잉 중 branch에서 `V2ModalDialog(activity = this, layoutInflater = layoutInflater, title = getString(...), desc = getString(..., creatorName), confirmButtonTitle = getString(...), confirmButtonClick = { viewModel.follow(creatorId, follow = false, notify = false) }, cancelButtonTitle = getString(...))`를 생성해 `show(screenWidth)`로 표시한다.
- [ ] **GREEN 확인:** `./gradlew :app:test --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"` 성공을 확인다.
- [ ] **REFACTOR:** 사용하지 않게 된 import와 이번 변경이 만든 중복만 제거다.
- [x] **RED:** `P1-T2` test 실패 상태를 확인다.
- [x] **RED 확인:** 실패 원인이 `CreatorFollowNotifyFragment` route와 Dialog 미사용임을 기록다.
- [x] **GREEN:** `onClickUnfollow` callback으로 클릭 대상 creator name을 전달하는 최소 경로를 정다.
- [x] **GREEN:** 팔로잉 리스트 Activity의 팔로잉 중 branch에서 `CreatorFollowNotifyFragment` 생성과 `show()` 호출을 제거다.
- [x] **GREEN:** 팔로잉 중 branch에서 `V2ModalDialog`를 생성해 `show(screenWidth)`로 표시하고 confirm callback에서만 기존 unfollow를 호출했다.
- [x] **GREEN 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"` 성공을 확인다.
- [x] **REFACTOR:** 사용하지 않게 된 import와 이번 변경이 만든 중복만 제거다.
## Phase 4. 크리에이터 채널 변경
@@ -259,13 +259,13 @@
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- [ ] **RED:** `P1-T3` test 실패 상태를 확인다.
- [ ] **RED 확인:** 실패 원인이 즉시 `homeActionDelegate?.follow(false, false)` 호출임을 기록다.
- [ ] **GREEN:** `!header.isFollow` branch는 기존 즉시 follow 동작으로 유지다.
- [ ] **GREEN:** `header.isFollow` branch에서 `V2ModalDialog`를 생성하고 `desc = getString(R.string.creator_unfollow_confirm_desc, header.nickname)`를 전달다.
- [ ] **GREEN:** confirm callback 안으로 기존 `homeActionDelegate?.follow(follow = false, notify = false)`를 이동다.
- [ ] **GREEN 확인:** `./gradlew :app:test --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 성공을 확인다.
- [ ] **REFACTOR:** `showDeleteCreatorChannelCommunityPostDialog()` 기존 `V2ModalDialog` 사용 패턴과 어긋나는 중복만 정리한다.
- [x] **RED:** `P1-T3` test 실패 상태를 확인다.
- [x] **RED 확인:** 실패 원인이 즉시 `homeActionDelegate?.follow(false, false)` 호출임을 기록다.
- [x] **GREEN:** `!header.isFollow` branch는 기존 즉시 follow 동작으로 유지다.
- [x] **GREEN:** `header.isFollow` branch에서 `V2ModalDialog`를 생성하고 `desc = getString(R.string.creator_unfollow_confirm_desc, header.nickname)`를 전달다.
- [x] **GREEN:** confirm callback 안으로 기존 `homeActionDelegate?.follow(follow = false, notify = false)`를 이동다.
- [x] **GREEN 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 성공을 확인다.
- [x] **REFACTOR:** 기존 `V2ModalDialog` 사용 패턴과 같은 방식으로만 추가했다.
## Phase 5. 검증
@@ -286,11 +286,11 @@
- **범위 밖:** 실패와 무관한 기능 수정, test 삭제·완화.
```bash
./gradlew :app:test --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"
./gradlew :app:test --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"
./gradlew :app:test --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
./gradlew :app:test --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest"
./gradlew :app:test --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"
```
**Expected:** 모든 명령이 exit code 0으로 종료되고, 팔로잉 리스트 Bottom Sheet route 제거, 양 화면 `V2ModalDialog` 확인 후 unfollow, 다국어 resource 계약이 검증된다.
@@ -356,3 +356,9 @@ P1-T3 ┘ └→ P4-T1 ───────┴→ P5-GATE
## Verification Log
- 2026-08-18: 문서 생성 작업만 수행했다. 사용자 요청에 따라 production/test 코드 구현과 Gradle test 실행은 수행하지 않았다.
- 2026-08-18: 사용자 승인 후 문서대로 구현했다. `visual-qa`, 기기·에뮬레이터 UI 조작, 스크린샷 검증은 사용자 요청에 따라 수행하지 않았고, 사용자가 직접 확인할 수 있도록 남겼다.
- 2026-08-18: RED 확인 - `FollowingCreatorAdapterSourceTest`는 기존 팔로잉 중 리소스 불일치로 실패했다. `FollowingCreatorActivitySourceTest``CreatorFollowNotifyFragment` route와 locale resource 누락으로 실패했다. `CreatorChannelActivitySourceTest.follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다`는 즉시 unfollow 구현 때문에 실패했다.
- 2026-08-18: GREEN 확인 - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorAdapterSourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialogTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"` 모두 성공했다.
- 2026-08-18: 회귀 검증 - `./gradlew :app:testDebugUnitTest`, `./gradlew :app:ktlintCheck`, `./gradlew :app:assembleDebug`, `git diff --check` 모두 성공했다. 병렬 Gradle 실행 중 한 차례 test result XML 쓰기 충돌이 있었고, 같은 명령을 순차 재실행해 성공했다.
- 2026-08-18: 리뷰 게이트 후속 - 영어 문구가 PRD의 `Youll`과 정확히 일치하는지 검증하는 test를 추가했다. RED에서 ASCII apostrophe 문구 불일치로 실패를 확인한 뒤 resource를 PRD와 맞췄고, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.following.FollowingCreatorActivitySourceTest"`, `./gradlew :app:ktlintCheck`, `git diff --check`, `./gradlew :app:testDebugUnitTest`, `./gradlew :app:assembleDebug` 성공을 확인했다.
- 2026-08-18: 사용자 수동 테스트 완료 - 사용자가 팔로잉 리스트와 크리에이터 채널의 실제 UI 흐름 수동 테스트를 완료했다고 보고했다. PRD 요구사항 추적표의 수동 검증 상태와 Phase 5 남은 조건을 완료로 갱신했다.
@@ -119,17 +119,17 @@ API 계약 변경은 없다. 기존 팔로우/언팔로우 endpoint와 payload
### 12.1 기능 수용 기준
- [ ] 팔로잉 리스트 팔로잉 중 버튼이 요청 아이콘, 색상, 원형 스타일을 사용한다. (`UNFOLLOW-001`, `UNFOLLOW-002`)
- [ ] 팔로잉 리스트 팔로잉 중 버튼이 `CreatorFollowNotifyFragment`를 표시하지 않는다. (`UNFOLLOW-003`)
- [ ] 팔로잉 리스트 팔로우 해제는 `V2ModalDialog` 확인 후에만 실행된다. (`UNFOLLOW-004`)
- [ ] 크리에이터 채널 팔로우 해제는 `V2ModalDialog` 확인 후에만 실행된다. (`UNFOLLOW-005`)
- [ ] 한국어, 일본어, 영어 문구가 확정 문구와 일치한다. (`UNFOLLOW-006`)
- [x] 팔로잉 리스트 팔로잉 중 버튼이 요청 아이콘, 색상, 원형 스타일을 사용한다. (`UNFOLLOW-001`, `UNFOLLOW-002`)
- [x] 팔로잉 리스트 팔로잉 중 버튼이 `CreatorFollowNotifyFragment`를 표시하지 않는다. (`UNFOLLOW-003`)
- [x] 팔로잉 리스트 팔로우 해제는 `V2ModalDialog` 확인 후에만 실행된다. (`UNFOLLOW-004`)
- [x] 크리에이터 채널 팔로우 해제는 `V2ModalDialog` 확인 후에만 실행된다. (`UNFOLLOW-005`)
- [x] 한국어, 일본어, 영어 문구가 확정 문구와 일치한다. (`UNFOLLOW-006`)
### 12.2 추적성 완료 기준
- [ ] 모든 확정 요구사항이 `plan-task.md`의 Task와 연결된다.
- [ ] 구현 전 계획 문서가 먼저 준비된다.
- [ ] 코드 구현은 별도 사용자 승인 후 `plan-task.md`에 따라 진행한다.
- [x] 모든 확정 요구사항이 `plan-task.md`의 Task와 연결된다.
- [x] 구현 전 계획 문서가 먼저 준비된다.
- [x] 코드 구현은 별도 사용자 승인 후 `plan-task.md`에 따라 진행한다.
## 13. Open Questions
@@ -141,10 +141,10 @@ API 계약 변경은 없다. 기존 팔로우/언팔로우 endpoint와 payload
| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|---|---|---:|---|---|---|
| `UNFOLLOW-001~002` | 변경 없음 | 1, 3 | `P1-T1`, `P3-T1` | `FollowingCreatorAdapterSourceTest` 또는 동등 source/local test | 범위 제외 |
| `UNFOLLOW-003~004` | 변경 없음 | 1, 3 | `P1-T2`, `P3-T2` | `FollowingCreatorActivitySourceTest` 또는 동등 source/local test | 범위 제외 |
| `UNFOLLOW-005` | 변경 없음 | 1, 4 | `P1-T3`, `P4-T1` | `CreatorChannelActivitySourceTest` | 범위 제외 |
| `UNFOLLOW-006` | 변경 없음 | 2 | `P2-T1` | string resource source/local test 또는 관련 source test | 범위 제외 |
| `UNFOLLOW-001~002` | 변경 없음 | 1, 3 | `P1-T1`, `P3-T1` | `FollowingCreatorAdapterSourceTest` 또는 동등 source/local test | 사용자 수동 테스트 완료 |
| `UNFOLLOW-003~004` | 변경 없음 | 1, 3 | `P1-T2`, `P3-T2` | `FollowingCreatorActivitySourceTest` 또는 동등 source/local test | 사용자 수동 테스트 완료 |
| `UNFOLLOW-005` | 변경 없음 | 1, 4 | `P1-T3`, `P4-T1` | `CreatorChannelActivitySourceTest` | 사용자 수동 테스트 완료 |
| `UNFOLLOW-006` | 변경 없음 | 2 | `P2-T1` | string resource source/local test 또는 관련 source test | 사용자 수동 테스트 완료 |
## 15. Decision Log
@@ -153,12 +153,13 @@ API 계약 변경은 없다. 기존 팔로우/언팔로우 endpoint와 payload
| `2026-08-18` | `DEC-001` | 확정 | 한국어 Dialog description은 `팔로우를 해제할까요?\n%1$s의 소식을 받을 수 없어요.`로 사용한다. | 사용자 인터뷰에서 A안 확정 | `UNFOLLOW-006`, `P2-T1` |
| `2026-08-18` | `DEC-002` | 확정 | 시각 보조 브라우저는 사용하지 않고 텍스트 기반 문서만 작성한다. | 사용자 응답 | 문서 작성 방식 |
| `2026-08-18` | `DEC-003` | 확정 | 이번 작업은 PRD와 구현 계획 문서 생성만 수행하고 코드 구현은 하지 않는다. | 사용자 명시 요청 | 전체 범위 |
| `2026-08-18` | `DEC-004` | 확정 | 구현 후 수동 테스트 완료 상태로 처리한다. | 사용자 완료 보고 | `UNFOLLOW-001~006`, `P5-GATE` |
## 16. 변경 관리
요구사항 변경 시 다음을 확인한다.
- [ ] Decision Log에 변경 이유와 날짜를 기록했다.
- [ ] 관련 요구사항 상태·본문·수용 기준을 갱신했다.
- [ ] `plan-task.md`의 범위·Files·체크박스·완료 증거를 코드 변경 전에 갱신했다.
- [ ] 기존 Progress·검증 기록을 삭제하거나 덮어쓰지 않았다.
- [x] Decision Log에 변경 이유와 날짜를 기록했다.
- [x] 관련 요구사항 상태·본문·수용 기준을 갱신했다.
- [x] `plan-task.md`의 범위·Files·체크박스·완료 증거를 코드 변경 전에 갱신했다.
- [x] 기존 Progress·검증 기록을 삭제하거나 덮어쓰지 않았다.