From 4ab57f80692f591949cdad34ec81aee49eec0f85 Mon Sep 17 00:00:00 2001 From: 1hyok Date: Wed, 18 Feb 2026 16:32:36 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat(ci):=20Firebase=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=EC=BB=A4=EB=B0=8B=20=EA=B7=9C=EC=B9=99?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Firebase App Distribution 워크플로우에 디버그 키스토어의 SHA-1 해시를 출력하는 단계를 추가했습니다. 이를 통해 카카오 개발자 센터에 키 해시를 등록하여, 배포된 빌드에서 카카오 로그인이 실패하는 문제를 해결할 수 있습니다. - `local.properties`에 `KAKAO_NATIVE_APP_KEY` 주입 시 성공 여부를 명확히 알 수 있도록 로그 메시지를 추가했습니다. - 커밋 메시지나 PR에 "Made with Cursor"와 같은 AI 도구 관련 문구를 포함하지 않도록 하는 새로운 규칙(`no-cursor-attribution-in-pr.mdc`)을 추가했습니다. --- .../workflow/no-cursor-attribution-in-pr.mdc | 18 ++++++++++++++++++ .../workflows/firebase_app_distribution.yml | 15 +++++++++++++++ CLAUDE.md | 1 + 3 files changed, 34 insertions(+) create mode 100644 .cursor/rules/workflow/no-cursor-attribution-in-pr.mdc diff --git a/.cursor/rules/workflow/no-cursor-attribution-in-pr.mdc b/.cursor/rules/workflow/no-cursor-attribution-in-pr.mdc new file mode 100644 index 00000000..b3dabad1 --- /dev/null +++ b/.cursor/rules/workflow/no-cursor-attribution-in-pr.mdc @@ -0,0 +1,18 @@ +--- +description: Do not include "Made with Cursor" or Cursor attribution in PRs or commits +alwaysApply: true +--- + +# No Cursor Attribution in PR / Commits + +**Do not include "Made with Cursor" or any Cursor branding in content that is committed or submitted to the repo.** + +## Applies to + +- **PR title and body**: Do not add "Made with Cursor", Cursor links, or similar attribution in pull request descriptions or titles. +- **Commit messages**: Do not add Cursor attribution in commit subject, body, or footer. +- **Code and docs**: Do not add "Made with Cursor" (or similar) in comments, README, or any file that would be part of a PR. + +## Rule + +When generating or suggesting text for PRs, commits, or repo-facing content, omit any line or block that promotes or attributes the content to Cursor (e.g. "Made with Cursor", Cursor badges, or Cursor marketing links). Keep content professional and tool-agnostic. diff --git a/.github/workflows/firebase_app_distribution.yml b/.github/workflows/firebase_app_distribution.yml index bc7e2c64..1192090a 100644 --- a/.github/workflows/firebase_app_distribution.yml +++ b/.github/workflows/firebase_app_distribution.yml @@ -33,6 +33,9 @@ jobs: echo "sdk.dir=$ANDROID_SDK_ROOT" > app/local.properties if [ -n "$KAKAO_NATIVE_APP_KEY" ]; then echo "KAKAO_NATIVE_APP_KEY=$KAKAO_NATIVE_APP_KEY" >> local.properties + echo "KAKAO_NATIVE_APP_KEY: injected into local.properties (secret present)" + else + echo "KAKAO_NATIVE_APP_KEY: not set in repo secrets — Kakao login will not work in this build" fi - name: Setup Gradle @@ -46,6 +49,18 @@ jobs: - name: Build Debug APK run: ./gradlew assembleDebug + # Print SHA-1 so it can be registered in Kakao Developers (키 해시). + # Without this hash, Kakao login fails with "카카오 로그인에 실패했습니다" on distributed builds. + - name: Print debug keystore SHA-1 for Kakao + run: | + KEYSTORE="$HOME/.android/debug.keystore" + if [ -f "$KEYSTORE" ]; then + echo "Register this SHA-1 in Kakao Developers (앱 설정 → 플랫폼 → Android → 키 해시):" + keytool -list -v -keystore "$KEYSTORE" -alias androiddebugkey -storepass android -keypass android 2>/dev/null | grep -i "SHA1:" || true + else + echo "Debug keystore not found at $KEYSTORE; SHA-1 may be from Gradle default." + fi + - name: Install Firebase CLI run: npm install -g firebase-tools diff --git a/CLAUDE.md b/CLAUDE.md index e8a10642..de6ce1bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,7 @@ Please refer to the following specific rule files: - @.cursor/rules/workflow/execute-dont-instruct.mdc - *Execute requested actions instead of only giving instructions* - @.cursor/rules/workflow/gemini-prompt-web-search.mdc - *Gemini prompts: always require web search, no project context, reply = prompt only* - @.cursor/rules/workflow/git-policy.mdc - *Git 작업 및 브랜치 관리 정책* +- @.cursor/rules/workflow/no-cursor-attribution-in-pr.mdc - *Do not include "Made with Cursor" in PRs or commits* - @.cursor/rules/workflow/pr-ci-checks.mdc - *PR/CI required checks — commitlint, ktlint, build* - @.cursor/rules/workflow/pull-request-review.mdc - *Pull Request 작성 및 코드 리뷰 규칙* From a1919a991f89edfe5ec02be75afce418f243a3fb Mon Sep 17 00:00:00 2001 From: 1hyok Date: Wed, 18 Feb 2026 16:37:17 +0900 Subject: [PATCH 2/9] =?UTF-8?q?docs:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=82=A4=20=ED=95=B4=EC=8B=9C=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=95=88=EB=82=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `README.md`: CI/CD를 통해 배포된 APK에서 카카오 로그인이 실패할 경우, 빌드 로그에 출력되는 SHA-1 키 해시를 Kakao Developers에 등록해야 한다는 안내를 추가했습니다. - `.github/workflows/firebase_app_distribution.yml`: CI 워크플로에서 `keytool` 대신 `./gradlew signingReport` 명령어를 사용하여 빌드 서명에 사용된 SHA-1 키 해시를 더 안정적으로 출력하도록 개선했습니다. --- .github/workflows/firebase_app_distribution.yml | 9 ++------- README.md | 5 ++++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/firebase_app_distribution.yml b/.github/workflows/firebase_app_distribution.yml index 1192090a..3711cc98 100644 --- a/.github/workflows/firebase_app_distribution.yml +++ b/.github/workflows/firebase_app_distribution.yml @@ -53,13 +53,8 @@ jobs: # Without this hash, Kakao login fails with "카카오 로그인에 실패했습니다" on distributed builds. - name: Print debug keystore SHA-1 for Kakao run: | - KEYSTORE="$HOME/.android/debug.keystore" - if [ -f "$KEYSTORE" ]; then - echo "Register this SHA-1 in Kakao Developers (앱 설정 → 플랫폼 → Android → 키 해시):" - keytool -list -v -keystore "$KEYSTORE" -alias androiddebugkey -storepass android -keypass android 2>/dev/null | grep -i "SHA1:" || true - else - echo "Debug keystore not found at $KEYSTORE; SHA-1 may be from Gradle default." - fi + echo "Register the SHA-1 below in Kakao Developers (앱 설정 → 플랫폼 → Android → 키 해시):" + ./gradlew signingReport --no-daemon 2>/dev/null | grep -i "SHA1:" || true - name: Install Firebase CLI run: npm install -g firebase-tools diff --git a/README.md b/README.md index be0a9555..eda79b3d 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,10 @@ KAKAO_NATIVE_APP_KEY=your_native_app_key_here ``` **배포 빌드(Firebase App Distribution 등):** -CI에서 빌드하는 APK에서도 카카오 로그인이 동작하게 하려면, GitHub 저장소 **Settings → Secrets and variables → Actions**에 `KAKAO_NATIVE_APP_KEY` 시크릿을 추가하세요. 워크플로에서 이 값을 `local.properties`에 넣어 빌드합니다. +1. GitHub **Settings → Secrets and variables → Actions**에 `KAKAO_NATIVE_APP_KEY` 시크릿을 추가하세요. +2. **키 해시(SHA-1) 등록:** 배포용 APK는 CI에서 사용하는 디버그 키스토어로 서명됩니다. Kakao 로그인이 "카카오 로그인에 실패했습니다"로 끝나면, [Kakao Developers](https://developers.kakao.com) **앱 설정 → 플랫폼 → Android → 키 해시**에 해당 빌드의 SHA-1을 등록해야 합니다. + - CI 워크플로의 **Print debug keystore SHA-1 for Kakao** 단계 로그에 출력된 SHA-1 값을 복사해 Kakao 콘솔에 등록하세요. + - 로컬 디버그 빌드용 SHA-1도 필요하면: `keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android` 로 확인 후 등록할 수 있습니다. > **참고**: `local.properties`는 `.gitignore`에 포함되어 있어 Git에 커밋되지 않습니다. 따라서 각 개발자/빌드 환경에서 위 값들을 직접 설정해야 합니다. From 3212258d8f9966df30e966d5611bf8e591d01f31 Mon Sep 17 00:00:00 2001 From: 1hyok Date: Wed, 18 Feb 2026 16:46:50 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat(auth):=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=ED=99=94=EB=A9=B4=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `TokenManager.isLoggedInFlow`를 구독하여 로그인/로그아웃 상태 변경을 감지하고, `null` (로딩 중), `true` (로그인), `false` (로그아웃) 상태에 따라 적절한 화면으로 자동 전환하는 로직을 구현했습니다. - **로그아웃 시 즉시 스플래시 화면으로 이동:** 어느 화면에 있든 로그아웃 상태가 감지되면 스플래시 화면으로 즉시 이동하여 사용자 경험을 개선했습니다. - **'root' 라우트 로직 개선:** 앱 시작 시 'root' 라우트에서 `isLoggedIn` 상태에 따라 'home' 또는 'splash'로 분기하도록 수정했습니다. - **홈 화면 보호:** 로그인 상태가 아닌 경우, 홈 화면이 잠시 표시되는 것을 방지하기 위해 빈 `Box`를 표시하고 스플래시 화면으로 리디렉션하도록 수정했습니다. - 코드 가독성과 재사용성을 높이기 위해 내비게이션 관련 헬퍼 함수(`ejectToSplashIfLoggedOut`, `dispatchFromRoot` 등)들을 추가했습니다. --- .../app/navigation/navgraph/NavGraph.kt | 159 ++++++++++-------- 1 file changed, 88 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt b/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt index 28183662..1090b6e6 100644 --- a/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt +++ b/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt @@ -12,20 +12,24 @@ import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import kotlinx.coroutines.flow.map import com.kuit.afternote.R import com.kuit.afternote.app.compositionlocal.DataProviderLocals import com.kuit.afternote.app.di.ReceiverAuthSessionEntryPoint @@ -69,12 +73,53 @@ import com.kuit.afternote.feature.setting.presentation.navgraph.settingNavGraph import com.kuit.afternote.feature.timeletter.presentation.navgraph.TimeLetterRoute import com.kuit.afternote.feature.timeletter.presentation.navgraph.timeLetterNavGraph import dagger.hilt.android.EntryPointAccessors -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext private const val TAG_FINGERPRINT = "FingerprintLogin" +private fun ejectToSplashIfLoggedOut(nav: NavHostController, isLoggedIn: Boolean?) { + if (isLoggedIn != false) return + val currentRoute = nav.currentBackStackEntry?.destination?.route + if (currentRoute?.contains("Splash") == true) return + nav.navigate(OnboardingRoute.SplashRoute) { + popUpTo(nav.graph.startDestinationId) { inclusive = true } + launchSingleTop = true + } +} + +private fun dispatchFromRoot(nav: NavHostController, isLoggedIn: Boolean?) { + when (isLoggedIn) { + true -> nav.navigate("home") { + popUpTo("root") { inclusive = true } + launchSingleTop = true + } + false -> nav.navigate(OnboardingRoute.SplashRoute) { + popUpTo("root") { inclusive = true } + launchSingleTop = true + } + null -> Unit + } +} + +private fun redirectFromHomeIfLoggedOut(nav: NavHostController, isLoggedIn: Boolean?) { + if (isLoggedIn != false) return + nav.navigate(OnboardingRoute.SplashRoute) { + launchSingleTop = true + popUpTo("home") { inclusive = true } + } +} + +private fun createBottomNavTabSelectedHandler(nav: NavHostController): (BottomNavItem) -> Unit = + { item -> + when (item) { + BottomNavItem.HOME -> nav.navigate("home") { launchSingleTop = true } + BottomNavItem.AFTERNOTE -> + nav.navigate(AfternoteRoute.FingerprintLoginRoute) { launchSingleTop = true } + BottomNavItem.RECORD -> nav.navigate("record_main") { launchSingleTop = true } + BottomNavItem.TIME_LETTER -> + nav.navigate(TimeLetterRoute.TimeLetterMainRoute) { launchSingleTop = true } + } + } + @Composable private fun FingerprintLoginRouteContent( navHostController: NavHostController, @@ -273,20 +318,13 @@ fun NavGraph(navHostController: NavHostController) { ReceiverAuthSessionEntryPoint::class.java ).receiverAuthSessionHolder() } - val scope = rememberCoroutineScope() - val navigateToUserMode: () -> Unit = { - scope.launch { - val token = tokenManager.getAccessToken() - withContext(Dispatchers.Main.immediate) { - if (!token.isNullOrEmpty()) { - navHostController.navigate("home") { - launchSingleTop = true - } - } else { - navHostController.navigate(OnboardingRoute.SplashRoute) - } - } - } + // null = still loading from DataStore, true/false = resolved + val isLoggedIn by remember(tokenManager) { + tokenManager.isLoggedInFlow.map { it } + }.collectAsStateWithLifecycle(initialValue = null) + + LaunchedEffect(isLoggedIn) { + ejectToSplashIfLoggedOut(navHostController, isLoggedIn) } val afternoteProvider = DataProviderLocals.LocalAfternoteEditDataProvider.current @@ -296,26 +334,7 @@ fun NavGraph(navHostController: NavHostController) { val playlistStateHolder = remember { MemorialPlaylistStateHolder() } var listRefreshRequested by remember { mutableStateOf(false) } - val onBottomNavTabSelected: (BottomNavItem) -> Unit = { item -> - when (item) { - BottomNavItem.HOME -> - navHostController.navigate("home") { - launchSingleTop = true - } - BottomNavItem.AFTERNOTE -> - navHostController.navigate(AfternoteRoute.FingerprintLoginRoute) { - launchSingleTop = true - } - BottomNavItem.RECORD -> - navHostController.navigate("record_main") { - launchSingleTop = true - } - BottomNavItem.TIME_LETTER -> - navHostController.navigate(TimeLetterRoute.TimeLetterMainRoute) { - launchSingleTop = true - } - } - } + val onBottomNavTabSelected = createBottomNavTabSelectedHandler(navHostController) NavHost( navController = navHostController, @@ -328,44 +347,42 @@ fun NavGraph(navHostController: NavHostController) { popExitTransition = { ExitTransition.None } ) { composable("root") { - LaunchedEffect(Unit) { - val token = tokenManager.getAccessToken() - withContext(Dispatchers.Main.immediate) { - if (!token.isNullOrEmpty()) { - navHostController.navigate("home") { - launchSingleTop = true - } - } else { - navHostController.navigate(OnboardingRoute.SplashRoute) - } - } + LaunchedEffect(isLoggedIn) { + dispatchFromRoot(navHostController, isLoggedIn) } } composable("home") { - HomeScreenContent( - onBottomNavTabSelected = onBottomNavTabSelected, - onSettingsClick = { - navHostController.navigate(SettingRoute.SettingMainRoute) { - launchSingleTop = true - } - }, - onDailyQuestionCtaClick = { - navHostController.navigate("record_main") { - launchSingleTop = true - } - }, - onAfternoteClick = { - navHostController.navigate(AfternoteRoute.FingerprintLoginRoute) { - launchSingleTop = true - } - }, - onTImeLetterClick = { - navHostController.navigate("record_main") { - launchSingleTop = true + LaunchedEffect(isLoggedIn) { + redirectFromHomeIfLoggedOut(navHostController, isLoggedIn) + } + if (isLoggedIn == true) { + HomeScreenContent( + onBottomNavTabSelected = onBottomNavTabSelected, + onSettingsClick = { + navHostController.navigate(SettingRoute.SettingMainRoute) { + launchSingleTop = true + } + }, + onDailyQuestionCtaClick = { + navHostController.navigate("record_main") { + launchSingleTop = true + } + }, + onAfternoteClick = { + navHostController.navigate(AfternoteRoute.FingerprintLoginRoute) { + launchSingleTop = true + } + }, + onTImeLetterClick = { + navHostController.navigate("record_main") { + launchSingleTop = true + } } - } - ) + ) + } else { + Box(Modifier.fillMaxSize()) + } } onboardingNavGraph( From fcee7289595db23dca3339ab8392991af42b08ae Mon Sep 17 00:00:00 2001 From: 1hyok Date: Wed, 18 Feb 2026 17:23:10 +0900 Subject: [PATCH 4/9] =?UTF-8?q?refactor(receiver):=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API?= =?UTF-8?q?=EC=97=90=EC=84=9C=20userId=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `getReceiverDetail` API 호출 시, 서버에서 인증 정보를 통해 사용자를 식별하므로 불필요한 `userId` 쿼리 파라미터를 제거했습니다. - `UserApiService`, `UserRepository`, `GetReceiverDetailUseCase`, `ReceiverDetailViewModel` 등 관련 계층의 함수 시그니처에서 `userId`를 모두 삭제하여 코드를 간소화했습니다. - `ReceiverDetailViewModelTest`에서 `GetUserIdUseCase` 모의(mock) 객체 및 관련 테스트 로직을 제거했습니다. - `ReceiverDetailViewModel`의 `loadReceiverDetail` 함수에서 `userId`를 가져오는 로직을 제거했습니다. - `ReceiverDetailRouteContent`에서 `hiltViewModel(backStackEntry)`를 사용하여 ViewModel의 범위를 내비게이션 그래프의 백 스택 엔트리로 지정하여, 화면이 다시 표시될 때 ViewModel이 재생성되지 않도록 수정했습니다. --- .../presentation/navgraph/SettingNavGraph.kt | 10 ++++-- .../feature/user/data/api/UserApiService.kt | 1 - .../feature/user/data/dto/UserDto.kt | 6 ++-- .../data/repository/UserRepositoryImpl.kt | 6 ++-- .../user/domain/repository/UserRepository.kt | 2 +- .../usecase/GetReceiverDetailUseCase.kt | 5 ++- .../viewmodel/ReceiverDetailViewModel.kt | 16 ++-------- .../viewmodel/ReceiverDetailViewModelTest.kt | 32 +++++++------------ 8 files changed, 30 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt index e2929db9..caef2246 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable @@ -83,7 +84,11 @@ fun NavGraphBuilder.settingNavGraph(navController: NavController) { PasswordModifyScreen(onBackClick = { navController.popBackStack() }) } composable { backStackEntry -> - ReceiverDetailRouteContent(navController, backStackEntry.toRoute()) + ReceiverDetailRouteContent( + navController = navController, + backStackEntry = backStackEntry, + route = backStackEntry.toRoute() + ) } composable { ProfileEditScreen( @@ -268,9 +273,10 @@ private fun SettingMainRouteContent(navController: NavController) { @Composable private fun ReceiverDetailRouteContent( navController: NavController, + backStackEntry: NavBackStackEntry, route: SettingRoute.ReceiverDetailRoute ) { - val detailViewModel: ReceiverDetailViewModel = hiltViewModel() + val detailViewModel: ReceiverDetailViewModel = hiltViewModel(backStackEntry) val detailState by detailViewModel.uiState.collectAsStateWithLifecycle() val phoneNumberState = rememberTextFieldState() diff --git a/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt b/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt index 277c3ea4..6975a3eb 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt @@ -82,7 +82,6 @@ interface UserApiService { */ @GET("users/receivers/{receiverId}") suspend fun getReceiverDetail( - @Query("userId") userId: Long, @Path("receiverId") receiverId: Long ): ApiResponse diff --git a/app/src/main/java/com/kuit/afternote/feature/user/data/dto/UserDto.kt b/app/src/main/java/com/kuit/afternote/feature/user/data/dto/UserDto.kt index 70a54b00..50e879c5 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/data/dto/UserDto.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/data/dto/UserDto.kt @@ -72,9 +72,9 @@ data class ReceiverDetailResponseDto( val relation: String, val phone: String? = null, val email: String? = null, - val dailyQuestionCount: Int, - val timeLetterCount: Int, - val afterNoteCount: Int + val dailyQuestionCount: Int = 0, + val timeLetterCount: Int = 0, + val afterNoteCount: Int = 0 ) // --- GET /users/receivers/{receiverId}/daily-questions (time-letters, after-notes are in Received API) --- diff --git a/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt index 4f86a6de..0fdae1df 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt @@ -124,10 +124,10 @@ class UserRepositoryImpl data.receiverId } - override suspend fun getReceiverDetail(userId: Long, receiverId: Long): Result = + override suspend fun getReceiverDetail(receiverId: Long): Result = runCatching { - Log.d(TAG, "getReceiverDetail: userId=$userId, receiverId=$receiverId") - val response = api.getReceiverDetail(userId = userId, receiverId = receiverId) + Log.d(TAG, "getReceiverDetail: receiverId=$receiverId") + val response = api.getReceiverDetail(receiverId = receiverId) Log.d(TAG, "getReceiverDetail: response=$response") UserMapper.toReceiverDetail(response.requireData()) } diff --git a/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt b/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt index 22788005..84984ad8 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt @@ -39,7 +39,7 @@ interface UserRepository { email: String? ): Result - suspend fun getReceiverDetail(userId: Long, receiverId: Long): Result + suspend fun getReceiverDetail(receiverId: Long): Result suspend fun getReceiverDailyQuestions( receiverId: Long, diff --git a/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/GetReceiverDetailUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/GetReceiverDetailUseCase.kt index 00305ebf..764e97ef 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/GetReceiverDetailUseCase.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/GetReceiverDetailUseCase.kt @@ -14,9 +14,8 @@ class GetReceiverDetailUseCase private val userRepository: UserRepository ) { /** - * @param userId 사용자 ID (query, required) * @param receiverId 수신인 식별자 (path, required) */ - suspend operator fun invoke(userId: Long, receiverId: Long): Result = - userRepository.getReceiverDetail(userId = userId, receiverId = receiverId) + suspend operator fun invoke(receiverId: Long): Result = + userRepository.getReceiverDetail(receiverId = receiverId) } diff --git a/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModel.kt index bbfefb41..b2de61b0 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.kuit.afternote.feature.setting.presentation.navgraph.SettingRoute import com.kuit.afternote.feature.user.domain.usecase.GetReceiverDetailUseCase -import com.kuit.afternote.feature.user.domain.usecase.GetUserIdUseCase import com.kuit.afternote.feature.user.presentation.uimodel.ReceiverDetailUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -25,8 +24,7 @@ class ReceiverDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - private val getReceiverDetailUseCase: GetReceiverDetailUseCase, - private val getUserIdUseCase: GetUserIdUseCase + private val getReceiverDetailUseCase: GetReceiverDetailUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(ReceiverDetailUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -46,18 +44,8 @@ class ReceiverDetailViewModel fun loadReceiverDetail(receiverId: Long) { viewModelScope.launch { - val userId = getUserIdUseCase() - if (userId == null) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "사용자 정보를 가져올 수 없습니다. 다시 로그인해주세요." - ) - } - return@launch - } _uiState.update { it.copy(isLoading = true, errorMessage = null) } - getReceiverDetailUseCase(userId = userId, receiverId = receiverId) + getReceiverDetailUseCase(receiverId = receiverId) .onSuccess { detail -> _uiState.update { it.copy( diff --git a/app/src/test/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModelTest.kt b/app/src/test/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModelTest.kt index 612455a1..1aae000e 100644 --- a/app/src/test/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModelTest.kt +++ b/app/src/test/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModelTest.kt @@ -3,7 +3,6 @@ package com.kuit.afternote.feature.user.presentation.viewmodel import androidx.lifecycle.SavedStateHandle import com.kuit.afternote.feature.user.domain.model.ReceiverDetail import com.kuit.afternote.feature.user.domain.usecase.GetReceiverDetailUseCase -import com.kuit.afternote.feature.user.domain.usecase.GetUserIdUseCase import com.kuit.afternote.util.MainCoroutineRule import io.mockk.coEvery import io.mockk.mockk @@ -31,13 +30,10 @@ class ReceiverDetailViewModelTest { val mainRule = MainCoroutineRule() private lateinit var getReceiverDetailUseCase: GetReceiverDetailUseCase - private lateinit var getUserIdUseCase: GetUserIdUseCase @Before fun setUp() { getReceiverDetailUseCase = mockk() - getUserIdUseCase = mockk() - coEvery { getUserIdUseCase() } returns 1L } private fun savedStateHandle(receiverId: String = ""): SavedStateHandle = @@ -49,8 +45,7 @@ class ReceiverDetailViewModelTest { val savedStateHandle = savedStateHandle(receiverId = "") val viewModel = ReceiverDetailViewModel( savedStateHandle, - getReceiverDetailUseCase, - getUserIdUseCase + getReceiverDetailUseCase ) advanceUntilIdle() @@ -71,12 +66,11 @@ class ReceiverDetailViewModelTest { timeLetterCount = 12, afterNoteCount = 4 ) - coEvery { getReceiverDetailUseCase(userId = 1L, receiverId = 1L) } returns Result.success(detail) + coEvery { getReceiverDetailUseCase(receiverId = 1L) } returns Result.success(detail) val savedStateHandle = savedStateHandle(receiverId = "1") val viewModel = ReceiverDetailViewModel( savedStateHandle, - getReceiverDetailUseCase, - getUserIdUseCase + getReceiverDetailUseCase ) viewModel.loadReceiverDetail(receiverId = 1L) advanceUntilIdle() @@ -94,12 +88,11 @@ class ReceiverDetailViewModelTest { val errorBody = """{"status":401,"code":401,"message":"Unauthorized"}""" .toResponseBody("application/json".toMediaType()) val httpException = HttpException(Response.error(401, errorBody)) - coEvery { getReceiverDetailUseCase(userId = any(), receiverId = any()) } returns Result.failure(httpException) + coEvery { getReceiverDetailUseCase(receiverId = any()) } returns Result.failure(httpException) val savedStateHandle = savedStateHandle(receiverId = "1") val viewModel = ReceiverDetailViewModel( savedStateHandle, - getReceiverDetailUseCase, - getUserIdUseCase + getReceiverDetailUseCase ) viewModel.loadReceiverDetail(receiverId = 1L) advanceUntilIdle() @@ -114,12 +107,11 @@ class ReceiverDetailViewModelTest { val errorBody = """{"status":404,"code":404,"message":"Receiver not found"}""" .toResponseBody("application/json".toMediaType()) val httpException = HttpException(Response.error(404, errorBody)) - coEvery { getReceiverDetailUseCase(userId = any(), receiverId = any()) } returns Result.failure(httpException) + coEvery { getReceiverDetailUseCase(receiverId = any()) } returns Result.failure(httpException) val savedStateHandle = savedStateHandle(receiverId = "1") val viewModel = ReceiverDetailViewModel( savedStateHandle, - getReceiverDetailUseCase, - getUserIdUseCase + getReceiverDetailUseCase ) viewModel.loadReceiverDetail(receiverId = 999L) advanceUntilIdle() @@ -134,12 +126,11 @@ class ReceiverDetailViewModelTest { val errorBody = """{"status":500,"code":500,"message":"Server error"}""" .toResponseBody("application/json".toMediaType()) val httpException = HttpException(Response.error(500, errorBody)) - coEvery { getReceiverDetailUseCase(userId = any(), receiverId = any()) } returns Result.failure(httpException) + coEvery { getReceiverDetailUseCase(receiverId = any()) } returns Result.failure(httpException) val savedStateHandle = savedStateHandle(receiverId = "1") val viewModel = ReceiverDetailViewModel( savedStateHandle, - getReceiverDetailUseCase, - getUserIdUseCase + getReceiverDetailUseCase ) viewModel.loadReceiverDetail(receiverId = 1L) advanceUntilIdle() @@ -151,14 +142,13 @@ class ReceiverDetailViewModelTest { @Test fun loadReceiverDetail_whenNetworkError_setsErrorMessage() = runTest { - coEvery { getReceiverDetailUseCase(userId = any(), receiverId = any()) } returns Result.failure( + coEvery { getReceiverDetailUseCase(receiverId = any()) } returns Result.failure( java.io.IOException("Network unavailable") ) val savedStateHandle = savedStateHandle(receiverId = "1") val viewModel = ReceiverDetailViewModel( savedStateHandle, - getReceiverDetailUseCase, - getUserIdUseCase + getReceiverDetailUseCase ) viewModel.loadReceiverDetail(receiverId = 1L) advanceUntilIdle() From 4e7e889b7ccec76b8af95e6848a021c80a28a320 Mon Sep 17 00:00:00 2001 From: 1hyok Date: Wed, 18 Feb 2026 17:50:14 +0900 Subject: [PATCH 5/9] =?UTF-8?q?refactor(receiver):=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=20=EB=93=B1=EB=A1=9D=20=EB=B0=8F=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=EC=9D=84=20`ReceiverFormScreen`=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `ReceiverRegisterScreen.kt` 파일을 삭제하고, 재사용 가능한 `ReceiverFormScreen.kt`를 새로 생성했습니다. - `ReceiverFormScreen`은 `screenTitle`과 `actionText` 파라미터를 받아 "수신자 등록"과 "수신자 수정" 화면을 모두 처리할 수 있도록 구현되었습니다. - `SettingNavGraph`에 수신자 수정을 위한 `ReceiverEditRoute`를 추가하고, 수신자 상세 화면의 '수정' 버튼 클릭 시 이 경로로 이동하도록 연결했습니다. --- .../presentation/navgraph/SettingNavGraph.kt | 29 +++++++++++--- .../presentation/navgraph/SettingRoute.kt | 5 +++ ...egisterScreen.kt => ReceiverFormScreen.kt} | 38 +++++++++++++------ 3 files changed, 56 insertions(+), 16 deletions(-) rename app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/{ReceiverRegisterScreen.kt => ReceiverFormScreen.kt} (90%) diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt index caef2246..d969c89e 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt @@ -50,7 +50,7 @@ import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverD import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverDetailScreen import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverDetailScreenParams import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverManagementScreen -import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverRegisterScreen +import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverFormScreen import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverTimeLetterListScreen import com.kuit.afternote.feature.setting.presentation.viewmodel.WithdrawalPasswordViewModel import com.kuit.afternote.feature.timeletter.presentation.component.LetterTheme @@ -108,6 +108,9 @@ fun NavGraphBuilder.settingNavGraph(navController: NavController) { composable { ReceiverRegisterRouteContent(navController) } + composable { + ReceiverEditRouteContent(navController) + } composable { PostDeliveryConditionScreen( onBackClick = { navController.popBackStack() } @@ -300,7 +303,11 @@ private fun ReceiverDetailRouteContent( ), callbacks = ReceiverDetailEditCallbacks( onBackClick = { navController.popBackStack() }, - onEditClick = { }, + onEditClick = { + navController.navigate( + SettingRoute.ReceiverEditRoute(receiverId = route.receiverId) + ) + }, onReceiverDetailImageClick = { }, onDailyQuestionClick = { navController.navigate( @@ -358,10 +365,22 @@ private fun ReceiverRegisterRouteContent(navController: NavController) { navController.popBackStack() } } - ReceiverRegisterScreen( + ReceiverFormScreen( + onBackClick = { navController.popBackStack() }, + onActionClick = { navController.popBackStack() }, + registerViewModel = registerViewModel, + screenTitle = "수신자 등록", + actionText = "등록" + ) +} + +@Composable +private fun ReceiverEditRouteContent(navController: NavController) { + ReceiverFormScreen( onBackClick = { navController.popBackStack() }, - onRegisterClick = { navController.popBackStack() }, - registerViewModel = registerViewModel + onActionClick = { navController.popBackStack() }, + screenTitle = "수신자 수정", + actionText = "수정" ) } diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingRoute.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingRoute.kt index 994f4fca..c71a1e23 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingRoute.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingRoute.kt @@ -44,6 +44,11 @@ sealed interface SettingRoute { @Serializable data object ReceiverRegisterRoute : SettingRoute + @Serializable + data class ReceiverEditRoute( + val receiverId: String = "" + ) : SettingRoute + @Serializable data object PostDeliveryConditionRoute : SettingRoute diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverRegisterScreen.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverFormScreen.kt similarity index 90% rename from app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverRegisterScreen.kt rename to app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverFormScreen.kt index c0b8240a..e08543b7 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverRegisterScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverFormScreen.kt @@ -44,12 +44,14 @@ import com.kuit.afternote.ui.theme.Gray1 import com.kuit.afternote.ui.theme.White @Composable -fun ReceiverRegisterScreen( +fun ReceiverFormScreen( modifier: Modifier = Modifier, onBackClick: () -> Unit, - onRegisterClick: () -> Unit = {}, + onActionClick: () -> Unit = {}, onAddProfileImageClick: () -> Unit = {}, - registerViewModel: RegisterReceiverViewModel? = null + registerViewModel: RegisterReceiverViewModel? = null, + screenTitle: String = "수신자 등록", + actionText: String = "등록" ) { val nameState = rememberTextFieldState() val phoneNumberState = rememberTextFieldState() @@ -58,7 +60,7 @@ fun ReceiverRegisterScreen( val relationshipOptions = listOf("딸", "아들", "친구", "가족", "연인", "동료", "기타") val dropdownState = rememberSelectionDropdownState() - val onRegisterAction: () -> Unit = { + val onPrimaryAction: () -> Unit = { if (registerViewModel != null) { registerViewModel.registerReceiver( name = nameState.text.toString().trim(), @@ -67,7 +69,7 @@ fun ReceiverRegisterScreen( email = emailState.text.toString().trim().takeIf { it.isNotBlank() } ) } else { - onRegisterClick() + onActionClick() } } @@ -76,10 +78,10 @@ fun ReceiverRegisterScreen( containerColor = Gray1, topBar = { TopBar( - title = "수신자 등록", + title = screenTitle, onBackClick = onBackClick, - onActionClick = onRegisterAction, - actionText = "등록" + onActionClick = onPrimaryAction, + actionText = actionText ) } ) { paddingValues -> @@ -205,12 +207,26 @@ private fun ProfileImageWithAddButton( @Preview(showBackground = true) @Composable -private fun ReceiverRegisterScreenPreview() { +private fun ReceiverFormScreenRegisterPreview() { AfternoteTheme { - ReceiverRegisterScreen( + ReceiverFormScreen( onBackClick = {}, - onRegisterClick = {}, + onActionClick = {}, onAddProfileImageClick = {} ) } } + +@Preview(showBackground = true) +@Composable +private fun ReceiverFormScreenEditPreview() { + AfternoteTheme { + ReceiverFormScreen( + onBackClick = {}, + onActionClick = {}, + onAddProfileImageClick = {}, + screenTitle = "수신자 수정", + actionText = "수정" + ) + } +} From c55a6f43e722d1021bbe3b5e3a14308db31818cc Mon Sep 17 00:00:00 2001 From: 1hyok Date: Wed, 18 Feb 2026 18:11:45 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat(receiver):=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수신자 정보를 수정하는 `EditReceiverViewModel`을 추가했습니다. 이 ViewModel은 `receiverId`를 받아 기존 수신자 정보를 불러오고(GET), `updateReceiver` 함수를 통해 이름, 관계, 연락처 등을 수정(PATCH)하는 기능을 수행합니다. - 수신자 수정을 위한 `UpdateReceiverUseCase`를 추가했습니다. - `UserRepository` 인터페이스 및 `UserRepositoryImpl` 구현체에 `updateReceiver` 함수를 추가하고, `UserApiService`에 `PATCH /users/receivers/{receiverId}` API 요청을 정의했습니다. - 수신자 수정 화면(`ReceiverFormScreen`)이 `EditReceiverViewModel`과 연동되도록 수정했습니다. 수정 화면 진입 시 기존 정보를 불러와 폼에 채우고, '수정' 버튼 클릭 시 ViewModel의 `updateReceiver` 함수를 호출합니다. 수정이 성공하면 이전 화면으로 자동 복귀합니다. - 수신자 등록 및 수정 화면의 재사용성을 높이기 위해, `ReceiverFormScreen`의 콜백과 초기 상태를 관리하는 `ReceiverFormCallbacks`와 `ReceiverFormInitialState` 데이터 클래스를 추가했습니다. --- .../presentation/navgraph/SettingNavGraph.kt | 49 ++++++-- .../screen/receiver/ReceiverFormScreen.kt | 82 ++++++++++--- .../feature/user/data/api/UserApiService.kt | 10 ++ .../data/repository/UserRepositoryImpl.kt | 29 +++++ .../user/domain/repository/UserRepository.kt | 8 ++ .../domain/usecase/UpdateReceiverUseCase.kt | 29 +++++ .../user/presentation/uimodel/UserUiState.kt | 11 ++ .../viewmodel/EditReceiverViewModel.kt | 114 ++++++++++++++++++ 8 files changed, 306 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/UpdateReceiverUseCase.kt create mode 100644 app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/EditReceiverViewModel.kt diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt index d969c89e..cb162e46 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt @@ -50,6 +50,8 @@ import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverD import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverDetailScreen import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverDetailScreenParams import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverManagementScreen +import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverFormCallbacks +import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverFormInitialState import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverFormScreen import com.kuit.afternote.feature.setting.presentation.screen.receiver.ReceiverTimeLetterListScreen import com.kuit.afternote.feature.setting.presentation.viewmodel.WithdrawalPasswordViewModel @@ -58,6 +60,7 @@ import com.kuit.afternote.feature.timeletter.presentation.uimodel.TimeLetterItem import com.kuit.afternote.feature.user.presentation.viewmodel.ProfileViewModel import com.kuit.afternote.feature.user.presentation.viewmodel.ReceiverDailyQuestionsViewModel import com.kuit.afternote.feature.user.presentation.viewmodel.ReceiverDetailViewModel +import com.kuit.afternote.feature.user.presentation.viewmodel.EditReceiverViewModel import com.kuit.afternote.feature.user.presentation.viewmodel.ReceiverListViewModel import com.kuit.afternote.feature.user.presentation.viewmodel.RegisterReceiverViewModel @@ -108,8 +111,11 @@ fun NavGraphBuilder.settingNavGraph(navController: NavController) { composable { ReceiverRegisterRouteContent(navController) } - composable { - ReceiverEditRouteContent(navController) + composable { backStackEntry -> + ReceiverEditRouteContent( + navController = navController, + backStackEntry = backStackEntry + ) } composable { PostDeliveryConditionScreen( @@ -200,7 +206,6 @@ private fun WithdrawalPasswordRouteContent(navController: NavController) { onBackClick = { navController.popBackStack() }, onWithdrawClick = { password -> withdrawalPasswordViewModel.submitWithdrawal(password) - // TODO: API 연동 후 실제 탈퇴 처리; 성공 시 withdrawalComplete = true로 설정 Log.d(TAG_SETTING_NAV, "Withdrawal password confirmed") }, onGoBackClick = { navController.popBackStack() }, @@ -366,8 +371,11 @@ private fun ReceiverRegisterRouteContent(navController: NavController) { } } ReceiverFormScreen( - onBackClick = { navController.popBackStack() }, - onActionClick = { navController.popBackStack() }, + callbacks = ReceiverFormCallbacks( + onBackClick = { navController.popBackStack() }, + onActionClick = { navController.popBackStack() }, + onAddProfileImageClick = {} + ), registerViewModel = registerViewModel, screenTitle = "수신자 등록", actionText = "등록" @@ -375,12 +383,35 @@ private fun ReceiverRegisterRouteContent(navController: NavController) { } @Composable -private fun ReceiverEditRouteContent(navController: NavController) { +private fun ReceiverEditRouteContent( + navController: NavController, + backStackEntry: NavBackStackEntry +) { + val editViewModel: EditReceiverViewModel = hiltViewModel(backStackEntry) + val editState by editViewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(editState.updateSuccess) { + if (editState.updateSuccess) { + editViewModel.clearUpdateSuccess() + navController.popBackStack() + } + } + ReceiverFormScreen( - onBackClick = { navController.popBackStack() }, - onActionClick = { navController.popBackStack() }, + callbacks = ReceiverFormCallbacks( + onBackClick = { navController.popBackStack() }, + onActionClick = {}, + onAddProfileImageClick = {} + ), + editViewModel = editViewModel, screenTitle = "수신자 수정", - actionText = "수정" + actionText = "수정", + initialFormState = ReceiverFormInitialState( + name = editState.name, + phone = editState.phone, + email = editState.email, + relation = editState.relation + ) ) } diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverFormScreen.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverFormScreen.kt index e08543b7..71a7eccc 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverFormScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverFormScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -38,20 +39,40 @@ import com.kuit.afternote.feature.afternote.presentation.component.edit.dropdown import com.kuit.afternote.feature.afternote.presentation.component.edit.dropdown.SelectionDropdown import com.kuit.afternote.feature.afternote.presentation.component.edit.dropdown.SelectionDropdownLabelParams import com.kuit.afternote.feature.afternote.presentation.component.edit.dropdown.rememberSelectionDropdownState +import com.kuit.afternote.feature.user.presentation.viewmodel.EditReceiverViewModel import com.kuit.afternote.feature.user.presentation.viewmodel.RegisterReceiverViewModel import com.kuit.afternote.ui.theme.AfternoteTheme import com.kuit.afternote.ui.theme.Gray1 import com.kuit.afternote.ui.theme.White +/** + * Callbacks for the receiver form screen (register / edit). + */ +data class ReceiverFormCallbacks( + val onBackClick: () -> Unit = {}, + val onActionClick: () -> Unit = {}, + val onAddProfileImageClick: () -> Unit = {} +) + +/** + * Pre-filled values for the receiver form (edit mode). + */ +data class ReceiverFormInitialState( + val name: String = "", + val phone: String = "", + val email: String = "", + val relation: String = "" +) + @Composable fun ReceiverFormScreen( modifier: Modifier = Modifier, - onBackClick: () -> Unit, - onActionClick: () -> Unit = {}, - onAddProfileImageClick: () -> Unit = {}, + callbacks: ReceiverFormCallbacks = ReceiverFormCallbacks(), registerViewModel: RegisterReceiverViewModel? = null, + editViewModel: EditReceiverViewModel? = null, screenTitle: String = "수신자 등록", - actionText: String = "등록" + actionText: String = "등록", + initialFormState: ReceiverFormInitialState = ReceiverFormInitialState() ) { val nameState = rememberTextFieldState() val phoneNumberState = rememberTextFieldState() @@ -59,27 +80,50 @@ fun ReceiverFormScreen( var relationshipSelectedValue by remember { mutableStateOf("딸") } val relationshipOptions = listOf("딸", "아들", "친구", "가족", "연인", "동료", "기타") val dropdownState = rememberSelectionDropdownState() + var initialApplied by remember { mutableStateOf(false) } + + LaunchedEffect( + initialFormState.name, + initialFormState.phone, + initialFormState.email, + initialFormState.relation + ) { + if (!initialApplied && initialFormState.name.isNotEmpty()) { + nameState.edit { replace(0, length, initialFormState.name) } + phoneNumberState.edit { replace(0, length, initialFormState.phone) } + emailState.edit { replace(0, length, initialFormState.email) } + relationshipSelectedValue = + if (initialFormState.relation in relationshipOptions) initialFormState.relation + else "딸" + initialApplied = true + } + } val onPrimaryAction: () -> Unit = { - if (registerViewModel != null) { - registerViewModel.registerReceiver( + when { + editViewModel != null -> editViewModel.updateReceiver( name = nameState.text.toString().trim(), relation = relationshipSelectedValue, phone = phoneNumberState.text.toString().trim().takeIf { it.isNotBlank() }, email = emailState.text.toString().trim().takeIf { it.isNotBlank() } ) - } else { - onActionClick() + registerViewModel != null -> registerViewModel.registerReceiver( + name = nameState.text.toString().trim(), + relation = relationshipSelectedValue, + phone = phoneNumberState.text.toString().trim().takeIf { it.isNotBlank() }, + email = emailState.text.toString().trim().takeIf { it.isNotBlank() } + ) + else -> callbacks.onActionClick() } } - BackHandler(onBack = onBackClick) + BackHandler(onBack = callbacks.onBackClick) Scaffold( containerColor = Gray1, topBar = { TopBar( title = screenTitle, - onBackClick = onBackClick, + onBackClick = callbacks.onBackClick, onActionClick = onPrimaryAction, actionText = actionText ) @@ -96,7 +140,7 @@ fun ReceiverFormScreen( // Profile Image with Add Button ProfileImageWithAddButton( - onAddClick = onAddProfileImageClick + onAddClick = callbacks.onAddProfileImageClick ) Spacer(modifier = Modifier.height(16.dp)) @@ -210,9 +254,11 @@ private fun ProfileImageWithAddButton( private fun ReceiverFormScreenRegisterPreview() { AfternoteTheme { ReceiverFormScreen( - onBackClick = {}, - onActionClick = {}, - onAddProfileImageClick = {} + callbacks = ReceiverFormCallbacks( + onBackClick = {}, + onActionClick = {}, + onAddProfileImageClick = {} + ) ) } } @@ -222,9 +268,11 @@ private fun ReceiverFormScreenRegisterPreview() { private fun ReceiverFormScreenEditPreview() { AfternoteTheme { ReceiverFormScreen( - onBackClick = {}, - onActionClick = {}, - onAddProfileImageClick = {}, + callbacks = ReceiverFormCallbacks( + onBackClick = {}, + onActionClick = {}, + onAddProfileImageClick = {} + ), screenTitle = "수신자 수정", actionText = "수정" ) diff --git a/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt b/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt index 6975a3eb..a19a10e2 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt @@ -85,6 +85,16 @@ interface UserApiService { @Path("receiverId") receiverId: Long ): ApiResponse + /** + * 수신인 수정. + * PATCH /users/receivers/{receiverId} — 특정 수신인 정보를 수정합니다. + */ + @PATCH("users/receivers/{receiverId}") + suspend fun updateReceiver( + @Path("receiverId") receiverId: Long, + @Body body: RegisterReceiverRequestDto + ): ApiResponse + /** * 수신인별 데일리 질문 답변 목록 조회 (페이지네이션). * GET /users/receivers/{receiverId}/daily-questions diff --git a/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt index 0fdae1df..6b074864 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt @@ -132,6 +132,35 @@ class UserRepositoryImpl UserMapper.toReceiverDetail(response.requireData()) } + override suspend fun updateReceiver( + receiverId: Long, + name: String, + relation: String, + phone: String?, + email: String? + ): Result = + runCatching { + Log.d(TAG, "updateReceiver: receiverId=$receiverId, name=$name") + val response = api.updateReceiver( + receiverId = receiverId, + body = RegisterReceiverRequestDto( + name = name, + relation = relation, + phone = phone, + email = email + ) + ) + Log.d(TAG, "updateReceiver: response=$response") + if (response.status !in 200..299) { + throw ApiException( + status = response.status, + code = response.code, + message = response.message + ) + } + Unit + } + override suspend fun getReceiverDailyQuestions( receiverId: Long, page: Int, diff --git a/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt b/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt index 84984ad8..9390d491 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt @@ -41,6 +41,14 @@ interface UserRepository { suspend fun getReceiverDetail(receiverId: Long): Result + suspend fun updateReceiver( + receiverId: Long, + name: String, + relation: String, + phone: String?, + email: String? + ): Result + suspend fun getReceiverDailyQuestions( receiverId: Long, page: Int, diff --git a/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/UpdateReceiverUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/UpdateReceiverUseCase.kt new file mode 100644 index 00000000..2cd021e9 --- /dev/null +++ b/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/UpdateReceiverUseCase.kt @@ -0,0 +1,29 @@ +package com.kuit.afternote.feature.user.domain.usecase + +import com.kuit.afternote.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +/** + * 수신자 수정 UseCase. + * PATCH /users/receivers/{receiverId} + */ +class UpdateReceiverUseCase + @Inject + constructor( + private val userRepository: UserRepository + ) { + suspend operator fun invoke( + receiverId: Long, + name: String, + relation: String, + phone: String? = null, + email: String? = null + ): Result = + userRepository.updateReceiver( + receiverId = receiverId, + name = name, + relation = relation, + phone = phone, + email = email + ) + } diff --git a/app/src/main/java/com/kuit/afternote/feature/user/presentation/uimodel/UserUiState.kt b/app/src/main/java/com/kuit/afternote/feature/user/presentation/uimodel/UserUiState.kt index f2454004..a57c10e4 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/presentation/uimodel/UserUiState.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/presentation/uimodel/UserUiState.kt @@ -57,6 +57,17 @@ data class ReceiverDetailUiState( val errorMessage: String? = null ) +data class EditReceiverUiState( + val receiverId: Long = 0L, + val name: String = "", + val relation: String = "", + val phone: String = "", + val email: String = "", + val isLoading: Boolean = false, + val errorMessage: String? = null, + val updateSuccess: Boolean = false +) + data class DailyQuestionAnswerItemUi( val dailyQuestionAnswerId: Long, val question: String, diff --git a/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/EditReceiverViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/EditReceiverViewModel.kt new file mode 100644 index 00000000..a9d2e483 --- /dev/null +++ b/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/EditReceiverViewModel.kt @@ -0,0 +1,114 @@ +package com.kuit.afternote.feature.user.presentation.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.kuit.afternote.feature.setting.presentation.navgraph.SettingRoute +import com.kuit.afternote.feature.user.domain.usecase.GetReceiverDetailUseCase +import com.kuit.afternote.feature.user.domain.usecase.UpdateReceiverUseCase +import com.kuit.afternote.feature.user.presentation.uimodel.EditReceiverUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * 수신자 수정 화면 ViewModel. + * GET /users/receivers/{receiverId} 로 상세 조회, PATCH /users/receivers/{receiverId} 로 수정. + */ +@HiltViewModel +class EditReceiverViewModel + @Inject + constructor( + savedStateHandle: SavedStateHandle, + private val getReceiverDetailUseCase: GetReceiverDetailUseCase, + private val updateReceiverUseCase: UpdateReceiverUseCase + ) : ViewModel() { + private val _uiState = MutableStateFlow(EditReceiverUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val receiverId: Long? = savedStateHandle.toRoute() + .receiverId.toLongOrNull() + + init { + receiverId?.let { loadReceiverDetail(it) } + } + + fun loadReceiverDetail(receiverId: Long) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + getReceiverDetailUseCase(receiverId = receiverId) + .onSuccess { detail -> + _uiState.update { + it.copy( + receiverId = detail.receiverId, + name = detail.name, + relation = detail.relation, + phone = detail.phone.orEmpty(), + email = detail.email.orEmpty(), + isLoading = false, + errorMessage = null + ) + } + } + .onFailure { e -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = e.message ?: "수신자 정보를 불러오지 못했습니다." + ) + } + } + } + } + + fun updateReceiver( + name: String, + relation: String, + phone: String?, + email: String? + ) { + val id = _uiState.value.receiverId + if (id == 0L) return + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null, updateSuccess = false) } + updateReceiverUseCase( + receiverId = id, + name = name, + relation = relation, + phone = phone?.takeIf { it.isNotBlank() }, + email = email?.takeIf { it.isNotBlank() } + ) + .onSuccess { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = null, + updateSuccess = true + ) + } + } + .onFailure { e -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = e.message ?: "수신자 수정에 실패했습니다.", + updateSuccess = false + ) + } + } + } + } + + fun clearUpdateSuccess() { + _uiState.update { it.copy(updateSuccess = false) } + } + + fun clearError() { + _uiState.update { it.copy(errorMessage = null) } + } + } From a9be17d1fb9c696a76b22457e923cefd2c8f0015 Mon Sep 17 00:00:00 2001 From: 1hyok Date: Wed, 18 Feb 2026 21:08:00 +0900 Subject: [PATCH 7/9] =?UTF-8?q?refactor(setting):=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B0=94=ED=85=80=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=8F=99=EC=9E=91=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `SettingMainScreen`, `SettingMainRouteContent`, `settingNavGraph`에 `onBottomNavTabSelected` 콜백을 추가하여, 설정 화면의 바텀 네비게이션 탭을 눌렀을 때 화면이 정상적으로 전환되도록 수정했습니다. - `NavGraph.kt`에서 `settingNavGraph`를 호출할 때 해당 콜백을 전달하도록 변경했습니다. --- .../app/navigation/navgraph/NavGraph.kt | 5 ++++- .../presentation/navgraph/SettingNavGraph.kt | 17 ++++++++++++++--- .../screen/main/SettingMainScreen.kt | 8 +++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt b/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt index 1090b6e6..e654b324 100644 --- a/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt +++ b/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt @@ -419,7 +419,10 @@ fun NavGraph(navHostController: NavHostController) { navController = navHostController, onNavItemSelected = onBottomNavTabSelected ) - settingNavGraph(navController = navHostController) + settingNavGraph( + navController = navHostController, + onBottomNavTabSelected = onBottomNavTabSelected + ) composable("receiver_main/{receiverId}") { backStackEntry -> val receiverId = backStackEntry.arguments?.getString("receiverId") ?: "1" diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt index cb162e46..8f11d813 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt @@ -19,6 +19,7 @@ import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.kuit.afternote.R import com.kuit.afternote.core.ui.component.ConfirmationPopup +import com.kuit.afternote.core.ui.component.navigation.BottomNavItem import com.kuit.afternote.core.ui.util.getAfternoteDisplayRes import com.kuit.afternote.core.uimodel.AfternoteListDisplayItem import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AfternoteEditReceiver @@ -64,9 +65,15 @@ import com.kuit.afternote.feature.user.presentation.viewmodel.EditReceiverViewMo import com.kuit.afternote.feature.user.presentation.viewmodel.ReceiverListViewModel import com.kuit.afternote.feature.user.presentation.viewmodel.RegisterReceiverViewModel -fun NavGraphBuilder.settingNavGraph(navController: NavController) { +fun NavGraphBuilder.settingNavGraph( + navController: NavController, + onBottomNavTabSelected: (BottomNavItem) -> Unit = {} +) { composable { - SettingMainRouteContent(navController) + SettingMainRouteContent( + navController = navController, + onBottomNavTabSelected = onBottomNavTabSelected + ) } composable { ConnectedAccountsScreen(onBackClick = { navController.popBackStack() }) @@ -225,7 +232,10 @@ private fun sampleNoticeItems(): List = List(10) { index -> private const val TAG_SETTING_NAV = "SettingNavGraph" @Composable -private fun SettingMainRouteContent(navController: NavController) { +private fun SettingMainRouteContent( + navController: NavController, + onBottomNavTabSelected: (BottomNavItem) -> Unit +) { val context = LocalContext.current val unhandledMessage = stringResource(R.string.setting_menu_not_connected) val logoutConfirmMessage = stringResource(R.string.setting_logout_confirm_message) @@ -255,6 +265,7 @@ private fun SettingMainRouteContent(navController: NavController) { } SettingMainScreen( + onBottomNavTabSelected = onBottomNavTabSelected, onClick = { title -> when (title) { "프로필 수정" -> navController.navigate(SettingRoute.ProfileEditRoute) diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/main/SettingMainScreen.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/main/SettingMainScreen.kt index 89cb63bc..1daeb16a 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/main/SettingMainScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/main/SettingMainScreen.kt @@ -27,6 +27,7 @@ import com.kuit.afternote.ui.theme.Sansneo @Composable fun SettingMainScreen( modifier: Modifier = Modifier, + onBottomNavTabSelected: (BottomNavItem) -> Unit = {}, onClick: (String) -> Unit ) { val sections = listOf( @@ -71,7 +72,12 @@ fun SettingMainScreen( topBar = { TopBar(title = "설정") }, - bottomBar = { BottomNavigationBar(selectedItem = BottomNavItem.HOME) { } } + bottomBar = { + BottomNavigationBar( + selectedItem = BottomNavItem.HOME, + onItemSelected = onBottomNavTabSelected + ) + } ) { paddingValues -> LazyColumn( modifier = modifier From b6743434ebdfb9a2af905ae3b8dbefa79871a091 Mon Sep 17 00:00:00 2001 From: 1hyok Date: Wed, 18 Feb 2026 21:36:49 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor(afternotedetail):=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=ED=99=95=EC=9D=B8=20=EB=8B=A4=EC=9D=B4=EC=96=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `GalleryDetailScreen.kt` 내부에 정의되어 있던 `GalleryDetailDeleteDialog`를 별도의 공용 컴포넌트인 `DeleteConfirmDialog`로 분리했습니다. - 이를 통해 `GalleryDetailScreen`의 코드를 간소화하고, 삭제 확인 다이얼로그의 재사용성을 높였습니다. --- .../afternotedetail/GalleryDetailScreen.kt | 69 +++---------------- 1 file changed, 9 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/kuit/afternote/core/ui/screen/afternotedetail/GalleryDetailScreen.kt b/app/src/main/java/com/kuit/afternote/core/ui/screen/afternotedetail/GalleryDetailScreen.kt index e60fb6bf..feb7d7e8 100644 --- a/app/src/main/java/com/kuit/afternote/core/ui/screen/afternotedetail/GalleryDetailScreen.kt +++ b/app/src/main/java/com/kuit/afternote/core/ui/screen/afternotedetail/GalleryDetailScreen.kt @@ -1,9 +1,6 @@ package com.kuit.afternote.core.ui.screen.afternotedetail import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -21,11 +18,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle @@ -36,7 +31,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kuit.afternote.R -import com.kuit.afternote.core.ui.component.detail.DeleteConfirmDialogContent +import com.kuit.afternote.core.ui.component.detail.DeleteConfirmDialog import com.kuit.afternote.core.ui.component.detail.EditDropdownMenu import com.kuit.afternote.core.ui.component.detail.InfoCard import com.kuit.afternote.core.ui.component.detail.ProcessingMethodItem @@ -175,60 +170,14 @@ private fun GalleryDetailContent( ) } - GalleryDetailDeleteDialog( - showDialog = uiState.showDeleteDialog, - serviceName = detailState.serviceName, - onDismiss = uiState::hideDeleteDialog, - onConfirm = { - uiState.hideDeleteDialog() - callbacks.onDeleteConfirm() - } - ) - } - } -} - -@Composable -private fun GalleryDetailDeleteDialog( - showDialog: Boolean, - serviceName: String, - onDismiss: () -> Unit, - onConfirm: () -> Unit -) { - if (!showDialog) return - - Box( - modifier = Modifier.fillMaxSize() - ) { - // 배경 오버레이 (반투명 검은색) - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.5f)) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { onDismiss() } - ) - - // 다이얼로그 컨텐츠 - 피그마 비율에 맞게 weight 사용 - Column( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - ) { - // 피그마 기준: 네비게이션 바로부터 247dp 비율에 맞게 weight 적용 - Spacer(modifier = Modifier.weight(1f)) - - Box( - modifier = Modifier - .padding(horizontal = 20.dp) - .fillMaxWidth() - ) { - DeleteConfirmDialogContent( - serviceName = serviceName, - onDismiss = onDismiss, - onConfirm = onConfirm + if (uiState.showDeleteDialog) { + DeleteConfirmDialog( + serviceName = detailState.serviceName, + onDismiss = uiState::hideDeleteDialog, + onConfirm = { + uiState.hideDeleteDialog() + callbacks.onDeleteConfirm() + } ) } } From b3a96423163498ecacc387b03754a5fb78dfcbb5 Mon Sep 17 00:00:00 2001 From: 1hyok Date: Wed, 18 Feb 2026 21:55:50 +0900 Subject: [PATCH 9/9] =?UTF-8?q?refactor(afternote):=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=20=EC=A7=80=EC=A0=95=20=EC=84=B9=EC=85=98=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EC=A0=80=EB=B8=94=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit '수신자 지정' 라벨과 수신자 목록을 포함하는 `RecipientDesignationSection` 컴포저블을 새로 추가하고, 이를 애프터노트 수정 화면의 여러 콘텐츠에 적용하여 UI 구조를 개선했습니다. - **`RecipientDesignationSection` 컴포저블 신규 추가** - "수신자 지정" 라벨과 `AfternoteEditReceiverList`를 묶어 재사용 가능한 컴포저블로 분리했습니다. - **기존 콘텐츠 리팩토링** - **`GalleryAndFileEditContent`**: 기존의 '정보 처리 방법'과 '수신자 추가' 로직을 제거하고, `RecipientDesignationSection`을 사용하도록 변경했습니다. - **`SocialNetworkEditContent`**: `recipientSection` 파라미터를 추가하여 수신자 지정 UI를 표시할 수 있도록 수정했습니다. - **`MemorialGuidelineEditContent`**: `MemorialGuidelineContent`의 슬롯에 `recipientContent`를 추가하고, `RecipientDesignationSection`을 통해 수신자 지정 UI를 표시하도록 업데이트했습니다. - 관련 `Params` 데이터 클래스들(`GalleryAndFileEditContentParams`, `SocialNetworkEditContentParams`, `MemorialGuidelineEditContentParams`)을 수정하여 `recipientSection`을 받도록 통일했습니다. --- .../content/MemorialGuidelineContent.kt | 4 + .../edit/content/GalleryAndFileEditContent.kt | 76 ++----------------- .../GalleryAndFileEditContentParams.kt | 4 +- .../content/MemorialGuidelineEditContent.kt | 3 + .../MemorialGuidelineEditContentParams.kt | 2 + .../content/RecipientDesignationSection.kt | 52 +++++++++++++ .../edit/content/SocialNetworkEditContent.kt | 5 ++ .../content/SocialNetworkEditContentParams.kt | 2 + .../screen/AfternoteEditScreen.kt | 27 +++---- 9 files changed, 85 insertions(+), 90 deletions(-) create mode 100644 app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/RecipientDesignationSection.kt diff --git a/app/src/main/java/com/kuit/afternote/core/ui/component/content/MemorialGuidelineContent.kt b/app/src/main/java/com/kuit/afternote/core/ui/component/content/MemorialGuidelineContent.kt index 9b0dca77..3faf4f2a 100644 --- a/app/src/main/java/com/kuit/afternote/core/ui/component/content/MemorialGuidelineContent.kt +++ b/app/src/main/java/com/kuit/afternote/core/ui/component/content/MemorialGuidelineContent.kt @@ -20,6 +20,7 @@ data class MemorialGuidelineSlots( val photoContent: @Composable () -> Unit, val playlistContent: @Composable () -> Unit, val lastWishContent: @Composable () -> Unit, + val recipientContent: @Composable () -> Unit = {}, val videoContent: @Composable () -> Unit ) @@ -48,6 +49,8 @@ fun MemorialGuidelineContent( Spacer(modifier = Modifier.height(sectionSpacing)) slots.lastWishContent() Spacer(modifier = Modifier.height(sectionSpacing)) + slots.recipientContent() + Spacer(modifier = Modifier.height(sectionSpacing)) slots.videoContent() if (trailingSpacerHeight > 0.dp) { Spacer(modifier = Modifier.height(trailingSpacerHeight)) @@ -65,6 +68,7 @@ private fun MemorialGuidelineContentPreview() { photoContent = { }, playlistContent = { }, lastWishContent = { }, + recipientContent = { }, videoContent = { } ) ) diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/GalleryAndFileEditContent.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/GalleryAndFileEditContent.kt index d6ac7fe7..c909589d 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/GalleryAndFileEditContent.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/GalleryAndFileEditContent.kt @@ -19,14 +19,10 @@ import com.kuit.afternote.core.ui.component.Label import com.kuit.afternote.core.ui.component.LabelStyle import com.kuit.afternote.core.ui.component.Multiline import com.kuit.afternote.core.ui.component.OutlineTextField -import com.kuit.afternote.feature.afternote.presentation.component.edit.afternoteeditreceiver.AfternoteEditReceiverList -import com.kuit.afternote.feature.afternote.presentation.component.edit.model.InfoMethodSection -import com.kuit.afternote.feature.afternote.presentation.component.edit.model.InformationProcessingMethod import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AfternoteEditReceiverSection +import com.kuit.afternote.feature.afternote.presentation.component.edit.model.ProcessingMethodSection import com.kuit.afternote.feature.afternote.presentation.component.edit.processingmethod.ProcessingMethodList import com.kuit.afternote.feature.afternote.presentation.component.edit.processingmethod.ProcessingMethodListParams -import com.kuit.afternote.core.ui.component.SelectableRadioCard -import com.kuit.afternote.feature.afternote.presentation.component.edit.processingmethod.OptionRadioCardContent import com.kuit.afternote.app.compositionlocal.DataProviderLocals import com.kuit.afternote.data.provider.FakeAfternoteEditDataProvider import com.kuit.afternote.ui.theme.AfternoteTheme @@ -65,63 +61,7 @@ private fun GalleryAndFileEditContentBody( spacerHeight: Dp ) { Column(modifier = modifier.fillMaxWidth()) { - // 정보 처리 방법 섹션 - Label( - text = "정보 처리 방법", - isRequired = true, - style = LabelStyle(requiredDotOffsetY = 4.dp) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - SelectableRadioCard( - selected = params.infoMethodSection.selectedMethod == InformationProcessingMethod.TRANSFER_TO_AFTERNOTE_EDIT_RECEIVER, - onClick = { params.infoMethodSection.onMethodSelected(InformationProcessingMethod.TRANSFER_TO_AFTERNOTE_EDIT_RECEIVER) }, - modifier = Modifier.fillMaxWidth(), - content = { - OptionRadioCardContent( - option = InformationProcessingMethod.TRANSFER_TO_AFTERNOTE_EDIT_RECEIVER, - selected = params.infoMethodSection.selectedMethod == InformationProcessingMethod.TRANSFER_TO_AFTERNOTE_EDIT_RECEIVER - ) - } - ) - - Spacer(modifier = Modifier.height(8.dp)) - - SelectableRadioCard( - selected = params.infoMethodSection.selectedMethod == InformationProcessingMethod.TRANSFER_TO_ADDITIONAL_AFTERNOTE_EDIT_RECEIVER, - onClick = { - params.infoMethodSection.onMethodSelected( - InformationProcessingMethod.TRANSFER_TO_ADDITIONAL_AFTERNOTE_EDIT_RECEIVER - ) - }, - modifier = Modifier.fillMaxWidth(), - content = { - OptionRadioCardContent( - option = InformationProcessingMethod.TRANSFER_TO_ADDITIONAL_AFTERNOTE_EDIT_RECEIVER, - selected = params.infoMethodSection.selectedMethod == InformationProcessingMethod.TRANSFER_TO_ADDITIONAL_AFTERNOTE_EDIT_RECEIVER - ) - } - ) - - // 추가 수신자에게 정보 전달 선택 시 수신자 추가 섹션 표시 - params.afternoteEditReceiverSection?.let { afternoteEditReceiverSection -> - Spacer(modifier = Modifier.height(32.dp)) - - // 수신자 추가 섹션 제목 - Label( - text = "수신자 추가", - isRequired = true, - style = LabelStyle(requiredDotOffsetY = 3.dp) - ) - - Spacer(modifier = Modifier.height(9.dp)) - - AfternoteEditReceiverList( - afternoteEditReceivers = afternoteEditReceiverSection.afternoteEditReceivers, - events = afternoteEditReceiverSection.callbacks - ) - } + RecipientDesignationSection(section = params.recipientSection) Spacer(modifier = Modifier.height(32.dp)) @@ -173,10 +113,8 @@ private fun GalleryAndFileEditContentPreview() { bottomPadding = PaddingValues(bottom = 88.dp), params = GalleryAndFileEditContentParams( messageState = rememberTextFieldState(), - infoMethodSection = InfoMethodSection( - selectedMethod = InformationProcessingMethod.TRANSFER_TO_AFTERNOTE_EDIT_RECEIVER, - onMethodSelected = {} - ) + recipientSection = AfternoteEditReceiverSection(), + processingMethodSection = ProcessingMethodSection() ) ) } @@ -200,11 +138,7 @@ private fun GalleryAndFileEditContentWithAfternoteEditReceiversPreview() { bottomPadding = PaddingValues(bottom = 88.dp), params = GalleryAndFileEditContentParams( messageState = rememberTextFieldState(), - infoMethodSection = InfoMethodSection( - selectedMethod = InformationProcessingMethod.TRANSFER_TO_ADDITIONAL_AFTERNOTE_EDIT_RECEIVER, - onMethodSelected = {} - ), - afternoteEditReceiverSection = AfternoteEditReceiverSection( + recipientSection = AfternoteEditReceiverSection( afternoteEditReceivers = provider.getAfternoteEditReceivers() ) ) diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/GalleryAndFileEditContentParams.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/GalleryAndFileEditContentParams.kt index 0cdde9a3..69db5aa7 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/GalleryAndFileEditContentParams.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/GalleryAndFileEditContentParams.kt @@ -2,7 +2,6 @@ package com.kuit.afternote.feature.afternote.presentation.component.edit.content import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Immutable -import com.kuit.afternote.feature.afternote.presentation.component.edit.model.InfoMethodSection import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AfternoteEditReceiverSection import com.kuit.afternote.feature.afternote.presentation.component.edit.model.ProcessingMethodSection @@ -12,7 +11,6 @@ import com.kuit.afternote.feature.afternote.presentation.component.edit.model.Pr @Immutable data class GalleryAndFileEditContentParams( val messageState: TextFieldState, - val infoMethodSection: InfoMethodSection, - val afternoteEditReceiverSection: AfternoteEditReceiverSection? = null, + val recipientSection: AfternoteEditReceiverSection, val processingMethodSection: ProcessingMethodSection = ProcessingMethodSection() ) diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContent.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContent.kt index ab2941c7..c3aea871 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContent.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContent.kt @@ -64,6 +64,9 @@ fun MemorialGuidelineEditContent( ) ) }, + recipientContent = { + params.recipientSection?.let { RecipientDesignationSection(section = it) } + }, videoContent = { FuneralVideoUpload( videoUrl = params.funeralVideoUrl, diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContentParams.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContentParams.kt index 81505025..b49c7d6b 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContentParams.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContentParams.kt @@ -3,6 +3,7 @@ package com.kuit.afternote.feature.afternote.presentation.component.edit.content import androidx.compose.runtime.Immutable import com.kuit.afternote.core.ui.component.LastWishOption import com.kuit.afternote.core.ui.component.list.AlbumCover +import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AfternoteEditReceiverSection /** * 추모 가이드라인 콘텐츠 파라미터 @@ -17,6 +18,7 @@ data class MemorialGuidelineEditContentParams( val funeralVideoUrl: String?, val funeralThumbnailUrl: String? = null, val customLastWishText: String, + val recipientSection: AfternoteEditReceiverSection? = null, val onSongAddClick: () -> Unit, val onLastWishSelected: (String) -> Unit, val onCustomLastWishChanged: (String) -> Unit, diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/RecipientDesignationSection.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/RecipientDesignationSection.kt new file mode 100644 index 00000000..34544268 --- /dev/null +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/RecipientDesignationSection.kt @@ -0,0 +1,52 @@ +package com.kuit.afternote.feature.afternote.presentation.component.edit.content + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.kuit.afternote.core.ui.component.Label +import com.kuit.afternote.core.ui.component.LabelStyle +import com.kuit.afternote.feature.afternote.presentation.component.edit.afternoteeditreceiver.AfternoteEditReceiverList +import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AfternoteEditReceiverCallbacks +import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AfternoteEditReceiverSection +import com.kuit.afternote.ui.theme.AfternoteTheme + +/** + * 수신자 지정 섹션 (Figma 수신자 지정 컴포넌트). + * 라벨 "수신자 지정"과 수신자 리스트를 표시합니다. + */ +@Composable +fun RecipientDesignationSection( + modifier: Modifier = Modifier, + section: AfternoteEditReceiverSection +) { + Column(modifier = modifier.fillMaxWidth()) { + Label( + text = "수신자 지정", + isRequired = true, + style = LabelStyle(requiredDotOffsetY = 3.dp) + ) + Spacer(modifier = Modifier.height(9.dp)) + AfternoteEditReceiverList( + afternoteEditReceivers = section.afternoteEditReceivers, + events = section.callbacks + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun RecipientDesignationSectionPreview() { + AfternoteTheme { + RecipientDesignationSection( + section = AfternoteEditReceiverSection( + afternoteEditReceivers = emptyList(), + callbacks = AfternoteEditReceiverCallbacks() + ) + ) + } +} diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/SocialNetworkEditContent.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/SocialNetworkEditContent.kt index 679ba914..20396552 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/SocialNetworkEditContent.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/SocialNetworkEditContent.kt @@ -139,6 +139,11 @@ private fun SocialNetworkEditContentContent( Spacer(modifier = Modifier.height(32.dp)) + params.recipientSection?.let { section -> + RecipientDesignationSection(section = section) + Spacer(modifier = Modifier.height(32.dp)) + } + // 처리 방법 리스트 섹션 Label( text = "처리 방법 리스트", diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/SocialNetworkEditContentParams.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/SocialNetworkEditContentParams.kt index 1da9f0f2..9648017d 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/SocialNetworkEditContentParams.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/SocialNetworkEditContentParams.kt @@ -3,6 +3,7 @@ package com.kuit.afternote.feature.afternote.presentation.component.edit.content import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Immutable import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AccountSection +import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AfternoteEditReceiverSection import com.kuit.afternote.feature.afternote.presentation.component.edit.model.ProcessingMethodSection /** @@ -12,5 +13,6 @@ import com.kuit.afternote.feature.afternote.presentation.component.edit.model.Pr data class SocialNetworkEditContentParams( val messageState: TextFieldState, val accountSection: AccountSection, + val recipientSection: AfternoteEditReceiverSection? = null, val processingMethodSection: ProcessingMethodSection = ProcessingMethodSection() ) diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditScreen.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditScreen.kt index c4859305..536beb0e 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditScreen.kt @@ -44,8 +44,6 @@ import com.kuit.afternote.feature.afternote.presentation.component.edit.afternot import com.kuit.afternote.feature.afternote.presentation.component.edit.afternoteeditreceiver.AddAfternoteEditReceiverDialogCallbacks import com.kuit.afternote.feature.afternote.presentation.component.edit.afternoteeditreceiver.AddAfternoteEditReceiverDialogParams import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AccountSection -import com.kuit.afternote.feature.afternote.presentation.component.edit.model.InfoMethodSection -import com.kuit.afternote.feature.afternote.presentation.component.edit.model.InformationProcessingMethod import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AfternoteEditReceiverSection import com.kuit.afternote.feature.afternote.presentation.component.edit.model.ProcessingMethodItem import com.kuit.afternote.feature.afternote.presentation.component.edit.model.ProcessingMethodSection @@ -379,6 +377,10 @@ private fun CategoryContent( funeralVideoUrl = state.funeralVideoUrl, funeralThumbnailUrl = state.funeralThumbnailUrl, customLastWishText = state.customLastWishText, + recipientSection = AfternoteEditReceiverSection( + afternoteEditReceivers = state.afternoteEditReceivers, + callbacks = state.galleryAfternoteEditReceiverCallbacks + ), onSongAddClick = onNavigateToAddSong, onLastWishSelected = state::onLastWishSelected, onCustomLastWishChanged = state::onCustomLastWishChanged, @@ -394,21 +396,10 @@ private fun CategoryContent( bottomPadding = bottomPadding, params = GalleryAndFileEditContentParams( messageState = state.messageState, - infoMethodSection = InfoMethodSection( - selectedMethod = state.selectedInformationProcessingMethod, - onMethodSelected = state::onInformationProcessingMethodSelected + recipientSection = AfternoteEditReceiverSection( + afternoteEditReceivers = state.afternoteEditReceivers, + callbacks = state.galleryAfternoteEditReceiverCallbacks ), - afternoteEditReceiverSection = if ( - state.selectedInformationProcessingMethod == - InformationProcessingMethod.TRANSFER_TO_ADDITIONAL_AFTERNOTE_EDIT_RECEIVER - ) { - AfternoteEditReceiverSection( - afternoteEditReceivers = state.afternoteEditReceivers, - callbacks = state.galleryAfternoteEditReceiverCallbacks - ) - } else { - null - }, processingMethodSection = ProcessingMethodSection( items = state.galleryProcessingMethods, callbacks = state.galleryProcessingCallbacks @@ -428,6 +419,10 @@ private fun CategoryContent( selectedMethod = state.selectedProcessingMethod, onMethodSelected = state::onProcessingMethodSelected ), + recipientSection = AfternoteEditReceiverSection( + afternoteEditReceivers = state.afternoteEditReceivers, + callbacks = state.galleryAfternoteEditReceiverCallbacks + ), processingMethodSection = ProcessingMethodSection( items = state.processingMethods, callbacks = state.socialProcessingCallbacks