Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
54f458b
feat: 최소 버전 강제 업데이트 모달 추가
latteeea May 12, 2026
da3b31d
fix: 쿼리 파라미터에 clientVersion, clientType 필드 추가
latteeea May 12, 2026
dc977bc
feat: 로컬 AlarmReadStore로 읽은 알림 id 저장 구현
latteeea May 12, 2026
6c14299
fix: alarmListScreen에서의 alarm 다루는 타입이 달라 생긴 충돌 해결
latteeea Jul 1, 2026
44077c0
fix: 강제 업데이트 모달창 로직 반대로 구현된 것 -> 바로
latteeea Jul 1, 2026
6a9519a
feat: 레드닷/개별 알림 구분 위해 로컬 storage 2개로 분리
latteeea Jul 1, 2026
9e499ef
feat: 알림 모두선택/선택삭제/모두삭제 기능 구현
latteeea Jul 1, 2026
e6c1a18
design: 일기 상세페이지 저장/공유/수정/삭제 아이콘 배치 및 수정/삭제 기능 옮김
latteeea Jul 1, 2026
8a2b3c9
feat: 내 컬렉션 저장하기 기능 1차 구현-사진 저장 테스트 완료
latteeea Jul 1, 2026
3fce01a
design: 내 컬렉션 저장 사진 디자인 파인튜닝
latteeea Jul 1, 2026
2b51b9f
fix: 수정 버튼 눌렀을때 4개 아이콘 없어지는 버그 수정
latteeea Jul 1, 2026
986277b
fix: 영상 일시정지일 때 저장 시 empty 사진으로 다운로드 되는 버그 수정 (최적화 문제)
latteeea Jul 1, 2026
6b79050
feat: 탐색페이지 저장하기 클릭 시 팝업창 구현
latteeea Jul 1, 2026
c3e0b0c
feat: 공유 기능 구현(카카오 테스트 불가, 인스타 기능 테스트 완료)
latteeea Jul 1, 2026
5355bd2
feat: 카카오톡 공유용 전용 리치 카드 개발 (but 비공개인 곡의 일기도 전달됨;;)
latteeea Jul 1, 2026
6ffb11f
feat: 카카오톡 공유 시 비공개 일기 내용 마스킹 처리
latteeea Jul 1, 2026
05b9a6e
feat: 카톡 리치 카드 -> 특정 다이어리로 이동 및 인스타 스토리 공유 기능 구현
latteeea Jul 1, 2026
befe568
fix: 탐색 및 소셜 페이지 스크롤 감도 조정(강도 상관없이 한 드래그에 한 페이지)
latteeea Jul 1, 2026
a102e05
chore: 스플래시 영상 변경
latteeea Jul 1, 2026
a8692bb
chore: 버전코드 변경
latteeea Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ android {
applicationId = "com.killingpart.killingpoint"
minSdk = 29
targetSdk = 36
versionCode = 40
versionName = "2.3.1"
versionCode = 41
versionName = "2.3.2"

buildConfigField("String", "AMPLITUDE_API_KEY", "\"fcb84a98b48f87f85e7112a1587976fd\"")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
Expand Down Expand Up @@ -60,6 +60,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
implementation("com.kakao.sdk:v2-user:2.21.4")
implementation("com.kakao.sdk:v2-share:2.21.4")
implementation("io.coil-kt:coil-compose:2.4.0")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- 카카오톡/인스타그램 공유(패키지 가시성, Android 11+) -->
<queries>
<package android:name="com.kakao.talk" />
<package android:name="com.instagram.android" />
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="kakaolink" />
</intent>
</queries>

<application
android:name=".KillingPointApplication"
android:usesCleartextTraffic="true"
Expand All @@ -31,6 +41,15 @@
android:supportsRtl="true"
android:theme="@style/Theme.KillingPoint"
android:hardwareAccelerated="true">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity
android:name=".MainActivity"
android:exported="true"
Expand All @@ -43,6 +62,16 @@

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<!-- 카카오톡 공유 카드 실행 파라미터로 앱 열기 (kakao{앱키}://kakaolink) -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:host="kakaolink"
android:scheme="kakaoe555c7ff865c9318e1672996f4481430" />
</intent-filter>
</activity>

<activity
Expand Down
Binary file modified app/src/main/assets/splash_video.mp4
Binary file not shown.
84 changes: 84 additions & 0 deletions app/src/main/java/com/killingpart/killingpoint/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.killingpart.killingpoint

import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.graphics.Color as AndroidColor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
Expand All @@ -18,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
Expand All @@ -28,6 +32,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.kakao.sdk.common.KakaoSdk
import com.killingpart.killingpoint.BuildConfig
Expand Down Expand Up @@ -61,6 +66,22 @@ class MainActivity : ComponentActivity() {
_pendingAlarmType.value = type
_pendingDeepLink.value = deepLink
}
handleKakaoLinkIntent(intent)
}

/**
* 카카오톡 공유 카드(실행 파라미터)로 앱이 열렸을 때 처리한다.
* data 예: kakao{앱키}://kakaolink?route=diary&diaryId=123
*/
private fun handleKakaoLinkIntent(intent: Intent) {
val data = intent.data ?: return
if (data.host != "kakaolink") return
val route = data.getQueryParameter("route")
val diaryId = data.getQueryParameter("diaryId")
if (route == "diary" && !diaryId.isNullOrBlank()) {
_pendingAlarmType.value = "DIARY_ALARM"
_pendingDeepLink.value = "/api/diaries/$diaryId"
}
}

override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -76,6 +97,7 @@ class MainActivity : ComponentActivity() {
_pendingAlarmType.value = type
_pendingDeepLink.value = deepLink
}
handleKakaoLinkIntent(intent)
}
enableEdgeToEdge()
window.statusBarColor = AndroidColor.BLACK
Expand Down Expand Up @@ -103,6 +125,9 @@ class MainActivity : ComponentActivity() {
var resolvedStartDestination by rememberSaveable {
mutableStateOf<String?>(null)
}
var showUpdateDialog by rememberSaveable {
mutableStateOf(false)
}

var previousLoginState by remember {
mutableStateOf<LoginUiState?>(null)
Expand Down Expand Up @@ -140,6 +165,7 @@ class MainActivity : ComponentActivity() {
val start = repo.getUserInitSettings()
.getOrNull()
?.let { init ->
showUpdateDialog = init.app.needsForceUpdate
when {
init.needsPolicyAgreement -> "onboarding_policy"
init.needsTagSetup -> "onboarding_name"
Expand All @@ -152,15 +178,18 @@ class MainActivity : ComponentActivity() {

is LoginUiState.Idle, is LoginUiState.Error -> {
resolvedStartDestination = "home"
showUpdateDialog = false
}

is LoginUiState.Success -> {
FcmTokenSync.syncCurrentToken(context)
resolvedStartDestination = "home"
showUpdateDialog = false
}

is LoginUiState.Loading -> {
resolvedStartDestination = null
showUpdateDialog = false
}
}
previousLoginState = loginState
Expand All @@ -183,6 +212,8 @@ class MainActivity : ComponentActivity() {

LaunchState.MAIN -> {
val navController = rememberNavController()
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentBackStackEntry?.destination?.route

val startDestination = resolvedStartDestination ?: "home"

Expand Down Expand Up @@ -251,6 +282,16 @@ class MainActivity : ComponentActivity() {
) { Text("마지막 화면") }
}
}

if (showUpdateDialog && currentRoute?.startsWith("main") == true) {
UpdateRequiredDialog(
onDismiss = { showUpdateDialog = false },
onUpdateClick = {
showUpdateDialog = false
openPlayStore(context)
}
)
}
}
}
}
Expand All @@ -275,3 +316,46 @@ class MainActivity : ComponentActivity() {
)
}
}

@Composable
private fun UpdateRequiredDialog(
onDismiss: () -> Unit,
onUpdateClick: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(text = "업데이트가 필요합니다.")
},
text = {
Text(text = "최신 버전으로 업데이트한 뒤 더 안정적으로 킬링파트를 이용해 주세요.")
},
confirmButton = {
TextButton(onClick = onUpdateClick) {
Text(text = "업데이트")
}
}
)
}

private fun openPlayStore(context: Context) {
val packageName = context.packageName
val marketIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$packageName")
).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val webIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=$packageName")
).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}

try {
context.startActivity(marketIntent)
} catch (_: ActivityNotFoundException) {
context.startActivity(webIntent)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.killingpart.killingpoint.data.local

import android.content.Context

object AlarmReadStore {
private const val PREF_NAME = "alarm_read_state"
// 텍스트 색상(흰색->회색)용: 사용자가 "개별로 탭한" 알림 ID만 저장
private const val KEY_READ_ALARM_IDS = "read_alarm_ids"
// 레드닷용: 알림 목록에 진입해 "화면에서 본" 알림 ID 저장 (탭 여부와 무관)
private const val KEY_SEEN_ALARM_IDS = "seen_alarm_ids"
// 로컬 삭제용: 서버 삭제 API가 없어 로컬에서 숨길 알림 ID 저장
private const val KEY_DELETED_ALARM_IDS = "deleted_alarm_ids"
private const val KEY_HAS_LOCAL_UNREAD = "has_local_unread"

// ---------- 텍스트 색상(개별 읽음) ----------

fun getReadAlarmIds(context: Context): Set<Long> {
return context.applicationContext
.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.getStringSet(KEY_READ_ALARM_IDS, emptySet())
.orEmpty()
.mapNotNull { it.toLongOrNull() }
.toSet()
}

/** 개별 알림 하나를 읽음(회색) 처리한다. 알림을 탭했을 때만 호출한다. */
fun markAlarmRead(context: Context, alarmId: Long) {
val preferences = context.applicationContext
.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
val updatedIds = preferences
.getStringSet(KEY_READ_ALARM_IDS, emptySet())
.orEmpty()
.toMutableSet()
.apply { add(alarmId.toString()) }

preferences.edit()
.putStringSet(KEY_READ_ALARM_IDS, updatedIds)
.apply()
}

// ---------- 레드닷(봤음) ----------

fun getSeenAlarmIds(context: Context): Set<Long> {
return context.applicationContext
.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.getStringSet(KEY_SEEN_ALARM_IDS, emptySet())
.orEmpty()
.mapNotNull { it.toLongOrNull() }
.toSet()
}

/**
* 알림 목록에 진입해 화면에 보여진 알림들을 "봤음"으로 저장한다.
* 레드닷만 끄고, 텍스트 색상(개별 읽음)에는 영향을 주지 않는다.
*/
fun markAlarmsSeen(context: Context, alarmIds: Collection<Long>) {
val appContext = context.applicationContext
val preferences = appContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
val updatedIds = preferences
.getStringSet(KEY_SEEN_ALARM_IDS, emptySet())
.orEmpty()
.toMutableSet()
.apply { addAll(alarmIds.map { it.toString() }) }

preferences.edit()
.putStringSet(KEY_SEEN_ALARM_IDS, updatedIds)
.putBoolean(KEY_HAS_LOCAL_UNREAD, false)
.apply()
}

fun hasUnread(context: Context, alarmIds: Collection<Long>): Boolean {
if (hasLocalUnread(context)) return true
if (alarmIds.isEmpty()) return false

val seenIds = getSeenAlarmIds(context)
return alarmIds.any { it !in seenIds }
}

// ---------- 로컬 삭제 ----------

fun getDeletedAlarmIds(context: Context): Set<Long> {
return context.applicationContext
.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.getStringSet(KEY_DELETED_ALARM_IDS, emptySet())
.orEmpty()
.mapNotNull { it.toLongOrNull() }
.toSet()
}

/**
* 알림들을 로컬에서 삭제(숨김) 처리한다.
* 삭제된 알림은 목록에서 제외되고, 다시 나타나 레드닷을 켜지 않도록 seen 에도 포함시킨다.
*/
fun markAlarmsDeleted(context: Context, alarmIds: Collection<Long>) {
if (alarmIds.isEmpty()) return

val preferences = context.applicationContext
.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
val idStrings = alarmIds.map { it.toString() }

val updatedDeleted = preferences
.getStringSet(KEY_DELETED_ALARM_IDS, emptySet())
.orEmpty()
.toMutableSet()
.apply { addAll(idStrings) }
val updatedSeen = preferences
.getStringSet(KEY_SEEN_ALARM_IDS, emptySet())
.orEmpty()
.toMutableSet()
.apply { addAll(idStrings) }

preferences.edit()
.putStringSet(KEY_DELETED_ALARM_IDS, updatedDeleted)
.putStringSet(KEY_SEEN_ALARM_IDS, updatedSeen)
.apply()
}

fun markLocalUnread(context: Context) {
context.applicationContext
.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_HAS_LOCAL_UNREAD, true)
.apply()
}

fun hasLocalUnread(context: Context): Boolean {
return context.applicationContext
.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.getBoolean(KEY_HAS_LOCAL_UNREAD, false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ interface ApiService {
@GET("users/init-settings")
suspend fun getUserInitSettings(
@Header("Authorization") accessToken: String,
@Query("clientType") clientType: String,
@Query("clientVersion") clientVersion: String
@Query("clientVersion") clientVersion: String,
@Query("clientType") clientType: String
): UserInitSettingsResponse

@POST("users/policy-agreement")
Expand Down
Loading