Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7332a1c
fix(afternote): 프로세스 사망 복원 뒤 프리필이 미저장 편집을 덮지 않게 한다 (#1732)
OnlyReadOneBook Sep 3, 2026
e53404f
fix(afternote): 프리필 가드를 「키 존재」가 아니라 「복원 성공」으로 판정한다 (#1732)
OnlyReadOneBook Sep 3, 2026
55a6ffa
merge: 부분 PATCH 기준 위에 프리필 복원 가드를 연결 (#1732)
OnlyReadOneBook Sep 8, 2026
bf0e5da
merge: 복원 프리필 보호를 최신 부분 PATCH와 Nav3 인자에 맞춤 (#1732)
OnlyReadOneBook Sep 8, 2026
0692ac1
Merge commit 'd939c2f7f03f6e6e74dc3bc93b6333015cda485c' into HEAD
OnlyReadOneBook Sep 8, 2026
99e6331
Merge commit '01c4be0d16fd5e0b04b9f97aeee2e7a53cd69612' into HEAD
OnlyReadOneBook Sep 8, 2026
65501d4
refactor(afternote): 합본 UserRepository 대신 좁은 계약 UserReceiverRepositor…
OnlyReadOneBook Sep 11, 2026
8021dde
chore: 옛 guard check-run 을 걷어내려 HEAD 를 갱신한다 (#1983)
OnlyReadOneBook Sep 11, 2026
056bd37
Merge remote-tracking branch 'origin/fix/1617-dirty-field-patch' into…
OnlyReadOneBook Sep 11, 2026
a84fd82
Merge remote-tracking branch 'origin/fix/1732-process-death-prefill-g…
OnlyReadOneBook Sep 11, 2026
e864441
Merge remote-tracking branch 'origin/fix/1617-dirty-field-patch' into…
OnlyReadOneBook Sep 13, 2026
967067e
Merge remote-tracking branch 'origin/fix/1732-process-death-prefill-g…
OnlyReadOneBook Sep 13, 2026
29c169e
Merge commit 'a2dd61836a6f45a2592c73d26a30de462012a851' into HEAD
OnlyReadOneBook Sep 20, 2026
fa1a330
Merge commit '29c169e0fbed2055cf18d628adf7eebdd37014c6' into HEAD
OnlyReadOneBook Sep 20, 2026
8dae413
test(afternote): restore process-death fixture with current snapshot …
OnlyReadOneBook Sep 20, 2026
ac3aca6
Merge commit '8dae413296b4a25186d29feb133a8b4b280c578d' into HEAD
OnlyReadOneBook Sep 20, 2026
920b088
chore(afternote): propagate validated snapshot fixture (#1732)
OnlyReadOneBook Sep 20, 2026
c9a809f
chore(afternote): propagate validated snapshot fixture (#1743)
OnlyReadOneBook Sep 20, 2026
10d43d5
refactor(afternote): 수정 입력 이름 변경 반영 (#1732)
OnlyReadOneBook Sep 25, 2026
8b58a15
refactor(afternote): 수정 입력 이름 변경 반영 (#1743)
OnlyReadOneBook Sep 25, 2026
d752cef
refactor(afternote): 마지막 말씀 DTO 변환명 전파 (#1732)
OnlyReadOneBook Sep 25, 2026
1b892fe
refactor(afternote): 마지막 말씀 DTO 변환명 전파 (#1743)
OnlyReadOneBook Sep 25, 2026
042354f
refactor(afternote): 카테고리별 비교 스냅샷 통합 (#1732)
OnlyReadOneBook Sep 25, 2026
9ec1b6f
refactor(afternote): 카테고리별 비교 스냅샷 통합 (#1743)
OnlyReadOneBook Sep 25, 2026
8abfcae
refactor(afternote): 수정 입력의 수신자 id 계약 통합 (#1732)
OnlyReadOneBook Sep 25, 2026
c26103a
refactor(afternote): 수정 입력의 수신자 id 계약 통합 (#1743)
OnlyReadOneBook Sep 25, 2026
2e2e337
refactor(afternote): 미디어 URL 인자 이름 통합 (#1732)
OnlyReadOneBook Sep 25, 2026
62cf979
refactor(afternote): 미디어 URL 인자 이름 통합 (#1743)
OnlyReadOneBook Sep 25, 2026
246df13
refactor(afternote): 좁은 저장소 변경을 프리필 가드 앞에 둔다 (#1743)
OnlyReadOneBook Sep 27, 2026
75a8b56
Merge remote-tracking branch 'origin/refactor/2166-media-mapping' int…
OnlyReadOneBook Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.afternote.core.common.reporting.ErrorReporter
import com.afternote.core.common.result.runCatchingCancellable
import com.afternote.core.domain.repository.MyProfileRepository
import com.afternote.core.domain.repository.UserProfileCacheRepository
import com.afternote.core.domain.repository.UserRepository
import com.afternote.feature.afternote.domain.repository.author.AfternoteRepository
import com.afternote.feature.afternote.presentation.R
import com.afternote.feature.afternote.presentation.navigation.model.AfternoteRoute
Expand All @@ -29,7 +29,7 @@ import kotlinx.coroutines.launch
* - 상세 조회: GET /api/afternotes/{id}
* - 삭제: DELETE /api/afternotes/{id}
* - 작성자 표시명: [UserProfileCacheRepository.getCachedUserName] 으로 즉시 채우고
* [UserRepository.getMyProfile] 로 재검증한다 (네비게이션 인자로 전달하지 않음)
* [MyProfileRepository.getMyProfile] 로 재검증한다 (네비게이션 인자로 전달하지 않음)
* - 상세 ID: entry 가 assisted 로 넘긴 [AfternoteRoute.DetailRoute]의 `itemId`. Nav3 entry 의
* SavedStateHandle 에는 NavKey 필드가 실리지 않으므로 `toRoute` 로 읽을 수 없다 (#2168).
*
Expand All @@ -47,7 +47,7 @@ class AfternoteDetailViewModel
constructor(
@Assisted private val route: AfternoteRoute.DetailRoute,
private val afternoteRepository: AfternoteRepository,
private val userRepository: UserRepository,
private val myProfileRepository: MyProfileRepository,
private val userProfileRepository: UserProfileCacheRepository,
private val errorReporter: ErrorReporter,
) : ViewModel() {
Expand Down Expand Up @@ -101,7 +101,7 @@ class AfternoteDetailViewModel
?.takeIf { it.isNotBlank() }
?.let(::applyAuthorDisplayName)

runCatchingCancellable { userRepository.getMyProfile() }
runCatchingCancellable { myProfileRepository.getMyProfile() }
.onSuccess { profile ->
applyAuthorDisplayName(profile.name)
runCatchingCancellable { userProfileRepository.saveUserName(profile.name) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.afternote.core.common.reporting.ErrorReporter
import com.afternote.core.common.result.runCatchingCancellable
import com.afternote.core.domain.repository.UserRepository
import com.afternote.core.domain.repository.UserReceiverRepository
import com.afternote.feature.afternote.domain.AfternoteType
import com.afternote.feature.afternote.domain.error.AfternoteFailure
import com.afternote.feature.afternote.domain.model.author.CreateAfternoteInput
Expand Down Expand Up @@ -176,7 +176,7 @@ class AfternoteEditorViewModel
constructor(
@Assisted private val route: AfternoteRoute.EditorFlowRoute,
private val savedStateHandle: SavedStateHandle,
private val userRepository: UserRepository,
private val userReceiverRepository: UserReceiverRepository,
private val afternoteRepository: AfternoteRepository,
private val memorialThumbnailUploadRepository: MemorialThumbnailUploadRepository,
private val resolveMemorialMediaForSave: ResolveMemorialMediaForSaveUseCase,
Expand Down Expand Up @@ -350,7 +350,7 @@ class AfternoteEditorViewModel
}

private suspend fun loadAuthorReceivers() {
runCatchingCancellable { userRepository.getReceivers() }
runCatchingCancellable { userReceiverRepository.getReceivers() }
.onSuccess { receivers ->
internalState.update { it.copy(authorReceivers = receivers.toAfternoteEditorReceivers()) }
}.onFailure { e ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.afternote.core.common.reporting.ErrorReporter
import com.afternote.core.common.result.runCatchingCancellable
import com.afternote.core.domain.repository.UserRepository
import com.afternote.core.domain.repository.UserReceiverRepository
import com.afternote.feature.afternote.presentation.editor.mapper.toAfternoteEditorReceivers
import com.afternote.feature.afternote.presentation.reporting.AfternoteFailureStage
import com.afternote.feature.afternote.presentation.reporting.recordAfternoteFailure
Expand Down Expand Up @@ -38,7 +38,7 @@ data class SelectReceiverUiState(
* 애프터노트 에디터의 수신자 선택 화면 ViewModel (#540).
*
* 서버 `GET users/receivers` 는 액세스 토큰으로 호출자를 식별하므로 파라미터가 없다 —
* 별도의 userId 없이 [UserRepository.getReceivers] 를 그대로 쓴다.
* 별도의 userId 없이 [UserReceiverRepository.getReceivers] 를 그대로 쓴다.
*
* 선택은 복수다 (#1426): 한 번 진입해 여러 명을 고르고, 완료 시 확정된 id 전체를
* `SELECTED_RECEIVER_IDS_KEY` 로 에디터에 돌려준다. 화면은 에디터 폼에 이미 담겨 있던 수신자를
Expand All @@ -53,7 +53,7 @@ data class SelectReceiverUiState(
class SelectReceiverViewModel
@Inject
constructor(
private val userRepository: UserRepository,
private val userReceiverRepository: UserReceiverRepository,
private val errorReporter: ErrorReporter,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
Expand Down Expand Up @@ -100,7 +100,7 @@ class SelectReceiverViewModel
fun refresh() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, loadFailed = false) }
runCatchingCancellable { userRepository.getReceivers() }
runCatchingCancellable { userReceiverRepository.getReceivers() }
.onSuccess { receivers ->
_uiState.update { state ->
state.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ private fun detailViewModel(
AfternoteDetailViewModel(
route = AfternoteRoute.DetailRoute(itemId = itemId),
afternoteRepository = repository,
userRepository = afternoteAuthorUserRepository(),
myProfileRepository = afternoteAuthorMyProfileRepository(),
userProfileRepository = afternoteAuthorUserProfileRepository(),
errorReporter = NoopAuthorErrorReporter,
)
Expand All @@ -516,7 +516,7 @@ private fun editorViewModel(
initialType = AfternoteType.SOCIAL_NETWORK,
itemId = itemId,
),
userRepository = afternoteAuthorUserRepository(),
userReceiverRepository = afternoteAuthorUserReceiverRepository(),
afternoteRepository = repository,
memorialThumbnailUploadRepository =
MemorialThumbnailUploadRepository {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ private fun implementedCoverageViewModel(
AfternoteEditorViewModel(
route = savedStateHandle.editorFlowRoute(),
savedStateHandle = savedStateHandle,
userRepository = afternoteAuthorUserRepository(),
userReceiverRepository = afternoteAuthorUserReceiverRepository(),
afternoteRepository = repository,
memorialThumbnailUploadRepository =
MemorialThumbnailUploadRepository {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class AfternoteAuthorTest {
AfternoteEditorViewModel(
route = savedStateHandle.editorFlowRoute(),
savedStateHandle = savedStateHandle,
userRepository = afternoteAuthorUserRepository(),
userReceiverRepository = afternoteAuthorUserReceiverRepository(),
afternoteRepository = repository,
memorialThumbnailUploadRepository = MemorialThumbnailUploadRepository { Result.success("https://cdn.test/thumb.jpg") },
resolveMemorialMediaForSave =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package com.afternote.feature.afternote.presentation

import androidx.lifecycle.SavedStateHandle
import com.afternote.core.common.reporting.ErrorReporter
import com.afternote.core.domain.testing.FakeMyProfileRepository
import com.afternote.core.domain.testing.FakeUserProfileCacheRepository
import com.afternote.core.domain.testing.FakeUserRepository
import com.afternote.core.domain.testing.FakeUserReceiverRepository
import com.afternote.core.model.user.Receiver
import com.afternote.core.model.user.User
import com.afternote.feature.afternote.domain.AfternoteType
Expand Down Expand Up @@ -33,17 +34,18 @@ internal fun afternoteEditorSavedStateHandle(
},
)

/** app androidTest 공용 helper가 열어 두던 UserRepository 경계만 그대로 허용한다. */
internal fun afternoteAuthorUserRepository(): FakeUserRepository =
FakeUserRepository.strict().apply {
profile = TEST_USER
/** 에디터·수신자 선택이 사용하는 수신자 조회 계약만 허용한다. */
internal fun afternoteAuthorUserReceiverRepository(): FakeUserReceiverRepository =
FakeUserReceiverRepository.strict().apply {
receiverState.value = listOf(TEST_RECEIVER)

onReceiverListFlow = null
onGetReceivers = null
onCreateReceiver = null
}

/** 작성자 상세가 사용하는 프로필 조회 계약만 허용한다. */
internal fun afternoteAuthorMyProfileRepository(): FakeMyProfileRepository =
FakeMyProfileRepository.strict().apply {
profile = TEST_USER
onGetMyProfile = null
onUpdateMyProfile = null
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ package com.afternote.feature.afternote.presentation.detail

import androidx.lifecycle.SavedStateHandle
import com.afternote.core.common.reporting.ErrorReporter
import com.afternote.core.domain.testing.FakeMyProfileRepository
import com.afternote.core.domain.testing.FakeUserProfileCacheRepository
import com.afternote.core.domain.testing.FakeUserRepository
import com.afternote.feature.afternote.domain.model.author.Detail
import com.afternote.feature.afternote.domain.model.author.DetailContent
import com.afternote.feature.afternote.domain.model.author.DetailCredentials
import com.afternote.feature.afternote.domain.model.author.DetailTimestamps
import com.afternote.feature.afternote.domain.testing.FakeAfternoteRepository
import com.afternote.feature.afternote.presentation.NoopAuthorErrorReporter
import com.afternote.feature.afternote.presentation.afternoteAuthorMyProfileRepository
import com.afternote.feature.afternote.presentation.afternoteAuthorUserProfileRepository
import com.afternote.feature.afternote.presentation.afternoteAuthorUserRepository
import com.afternote.feature.afternote.presentation.navigation.model.AfternoteRoute
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -162,8 +162,8 @@ class AfternoteDetailViewModelTest {
// 상세와 표시명은 출처가 다른 두 요청이라 도착 순서가 뒤집힌다. 이름이 늦는 쪽이 실제 경로 —
// 그때 이미 그려진 Success 에 이름이 실려야 제목의 이름 세그먼트가 뒤늦게라도 채워진다.
val profileGate = CompletableDeferred<Unit>()
val userRepository =
afternoteAuthorUserRepository().apply {
val myProfileRepository =
afternoteAuthorMyProfileRepository().apply {
onGetMyProfile = {
profileGate.await()
profile
Expand All @@ -173,15 +173,15 @@ class AfternoteDetailViewModelTest {
FakeAfternoteRepository.strict().apply {
onGetDetail = { Result.success(detail(serviceName = "Instagram")) }
}
val viewModel = viewModel(repository, userRepository = userRepository)
val viewModel = viewModel(repository, myProfileRepository = myProfileRepository)
val states = recordStates(viewModel)

// 이름이 도착하기 전 — 화면은 이미 상세를 그리고 있고 이름 자리는 비어 있다.
assertEquals("", states.last().authorDisplayNameOrNull())

profileGate.complete(Unit)

assertEquals(userRepository.profile.name, states.last().authorDisplayNameOrNull())
assertEquals(myProfileRepository.profile.name, states.last().authorDisplayNameOrNull())
}

@Test
Expand All @@ -198,25 +198,25 @@ class AfternoteDetailViewModelTest {
FakeAfternoteRepository.strict().apply {
onGetDetail = { results.removeFirst() }
}
val userRepository = afternoteAuthorUserRepository()
val viewModel = viewModel(repository, userRepository = userRepository)
val myProfileRepository = afternoteAuthorMyProfileRepository()
val viewModel = viewModel(repository, myProfileRepository = myProfileRepository)
val states = recordStates(viewModel)

viewModel.refreshOnReturn() // 첫 진입의 ON_RESUME — 스킵
viewModel.refreshOnReturn() // 백스택 복귀의 ON_RESUME

// 갱신이 만든 새 Success 도 이름을 들고 있어야 제목이 «…에 대한 기록» 으로 되돌아가지 않는다.
assertEquals("Threads", states.last().serviceNameOrNull())
assertEquals(userRepository.profile.name, states.last().authorDisplayNameOrNull())
assertEquals(myProfileRepository.profile.name, states.last().authorDisplayNameOrNull())
}

@Test
fun `캐시된 이름은 원격 응답을 기다리지 않고 제목에 실린다`() =
runTest {
// 원격만 쓰면 왕복이 끝나야 이름 세그먼트가 채워져, 진입마다 제목이 눈앞에서 다시 쓰인다.
val profileGate = CompletableDeferred<Unit>()
val userRepository =
afternoteAuthorUserRepository().apply {
val myProfileRepository =
afternoteAuthorMyProfileRepository().apply {
onGetMyProfile = {
profileGate.await()
profile.copy(name = "서버 이름")
Expand All @@ -228,7 +228,7 @@ class AfternoteDetailViewModelTest {
FakeAfternoteRepository.strict().apply {
onGetDetail = { Result.success(detail(serviceName = "Instagram")) }
},
userRepository = userRepository,
myProfileRepository = myProfileRepository,
userProfileRepository = userProfileRepository,
)
val states = recordStates(viewModel)
Expand All @@ -250,18 +250,18 @@ class AfternoteDetailViewModelTest {
afternoteAuthorUserProfileRepository().apply {
onGetCachedUserName = { throw IOException("DataStore 읽기 실패") }
}
val userRepository = afternoteAuthorUserRepository()
val myProfileRepository = afternoteAuthorMyProfileRepository()
val viewModel =
viewModel(
FakeAfternoteRepository.strict().apply {
onGetDetail = { Result.success(detail(serviceName = "Instagram")) }
},
userRepository = userRepository,
myProfileRepository = myProfileRepository,
userProfileRepository = userProfileRepository,
)
val states = recordStates(viewModel)

assertEquals(userRepository.profile.name, states.last().authorDisplayNameOrNull())
assertEquals(myProfileRepository.profile.name, states.last().authorDisplayNameOrNull())
}

@Test
Expand All @@ -272,18 +272,18 @@ class AfternoteDetailViewModelTest {
afternoteAuthorUserProfileRepository().apply {
onSaveUserName = { throw IOException("DataStore 쓰기 실패") }
}
val userRepository = afternoteAuthorUserRepository()
val myProfileRepository = afternoteAuthorMyProfileRepository()
val viewModel =
viewModel(
FakeAfternoteRepository.strict().apply {
onGetDetail = { Result.success(detail(serviceName = "Instagram")) }
},
userRepository = userRepository,
myProfileRepository = myProfileRepository,
userProfileRepository = userProfileRepository,
)
val states = recordStates(viewModel)

assertEquals(userRepository.profile.name, states.last().authorDisplayNameOrNull())
assertEquals(myProfileRepository.profile.name, states.last().authorDisplayNameOrNull())
assertTrue(states.last() is AfternoteDetailUiState.Success)
}

Expand Down Expand Up @@ -419,13 +419,13 @@ class AfternoteDetailViewModelTest {
private fun viewModel(
repository: FakeAfternoteRepository,
errorReporter: ErrorReporter = NoopAuthorErrorReporter,
userRepository: FakeUserRepository = afternoteAuthorUserRepository(),
myProfileRepository: FakeMyProfileRepository = afternoteAuthorMyProfileRepository(),
userProfileRepository: FakeUserProfileCacheRepository = afternoteAuthorUserProfileRepository(),
): AfternoteDetailViewModel =
AfternoteDetailViewModel(
route = AfternoteRoute.DetailRoute(itemId = 73L),
afternoteRepository = repository,
userRepository = userRepository,
myProfileRepository = myProfileRepository,
userProfileRepository = userProfileRepository,
errorReporter = errorReporter,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.afternote.feature.afternote.presentation.editor

import androidx.lifecycle.SavedStateHandle
import com.afternote.core.common.reporting.ErrorReporter
import com.afternote.core.domain.repository.UserRepository
import com.afternote.core.domain.repository.UserReceiverRepository
import com.afternote.feature.afternote.domain.AfternoteType
import com.afternote.feature.afternote.domain.repository.author.AfternoteRepository
import com.afternote.feature.afternote.domain.repository.author.MemorialMediaUploadRepository
Expand Down Expand Up @@ -163,7 +163,7 @@ class AfternoteEditorMemorialMediaRemoveTest {
AfternoteEditorViewModel(
route = savedStateHandle.editorFlowRoute(),
savedStateHandle = savedStateHandle,
userRepository = repositoryProxy<UserRepository>(),
userReceiverRepository = repositoryProxy<UserReceiverRepository>(),
afternoteRepository = repositoryProxy<AfternoteRepository>(),
memorialThumbnailUploadRepository =
MemorialThumbnailUploadRepository { error("썸네일 업로드가 호출되면 안 됩니다") },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.afternote.feature.afternote.presentation.editor

import androidx.lifecycle.SavedStateHandle
import com.afternote.core.common.reporting.ErrorReporter
import com.afternote.core.domain.testing.FakeUserRepository
import com.afternote.core.domain.testing.FakeUserReceiverRepository
import com.afternote.feature.afternote.domain.AfternoteType
import com.afternote.feature.afternote.domain.model.author.Detail
import com.afternote.feature.afternote.domain.model.author.DetailContent
Expand Down Expand Up @@ -149,7 +149,7 @@ class AfternoteEditorMissingBaselineTest {
"itemId" to EDIT_ID,
),
),
userRepository = FakeUserRepository.strict(),
userReceiverRepository = FakeUserReceiverRepository.strict(),
afternoteRepository = afternoteRepository,
memorialThumbnailUploadRepository =
MemorialThumbnailUploadRepository { error("썸네일 업로드가 호출되면 안 됩니다") },
Expand Down
Loading
Loading