Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions .cursor/rules/workflow/no-cursor-attribution-in-pr.mdc
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions .github/workflows/firebase_app_distribution.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,6 +49,13 @@ 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: |
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

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 작성 및 코드 리뷰 규칙*

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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에 커밋되지 않습니다. 따라서 각 개발자/빌드 환경에서 위 값들을 직접 설정해야 합니다.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@ 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.LaunchedEffect
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
Expand Down Expand Up @@ -61,20 +64,62 @@ import com.kuit.afternote.feature.receiver.presentation.navgraph.ReceiverMainRou
import com.kuit.afternote.feature.receiver.presentation.navgraph.ReceiverTimeLetterDetailRoute
import com.kuit.afternote.feature.receiver.presentation.navgraph.ReceiverTimeLetterRoute
import com.kuit.afternote.feature.receiver.presentation.screen.afternote.ReceiverAfternoteListEvent
import com.kuit.afternote.feature.receiver.presentation.uimodel.ReceiverAfternoteListUiState
import com.kuit.afternote.feature.receiverauth.screen.ReceiverOnboardingScreen
import com.kuit.afternote.feature.receiverauth.screen.VerifySelfScreen
import com.kuit.afternote.feature.receiver.presentation.uimodel.ReceiverAfternoteListUiState
import com.kuit.afternote.feature.setting.presentation.navgraph.SettingRoute
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
import kotlinx.coroutines.flow.map

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,
Expand Down Expand Up @@ -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<Boolean, Boolean?> { it }
}.collectAsStateWithLifecycle(initialValue = null)

LaunchedEffect(isLoggedIn) {
ejectToSplashIfLoggedOut(navHostController, isLoggedIn)
}

val afternoteProvider = DataProviderLocals.LocalAfternoteEditDataProvider.current
Expand All @@ -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,
Expand All @@ -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(TimeLetterRoute.TimeLetterMainRoute) {
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(TimeLetterRoute.TimeLetterMainRoute) {
launchSingleTop = true
}
}
}
)
)
} else {
Box(Modifier.fillMaxSize())
}
}

onboardingNavGraph(
Expand Down Expand Up @@ -402,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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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))
Expand All @@ -65,6 +68,7 @@ private fun MemorialGuidelineContentPreview() {
photoContent = { },
playlistContent = { },
lastWishContent = { },
recipientContent = { },
videoContent = { }
)
)
Expand Down
Loading
Loading