Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
93f165b
Feat: #110 케어콜 푸시알림 설정(잠금화면 시에도 뜨도록)
alswlekk Sep 21, 2025
ad4a373
Merge branch 'develop' of github.com:Medicare-Call/Medicare-Call-Andr…
alswlekk Sep 21, 2025
68042f2
Merge branch 'develop' into feat/fcm-alarm-#110
alswlekk Sep 21, 2025
9da1e74
Feat: #110 hiltManager 관련 의존성 추가
alswlekk Sep 22, 2025
dac99e8
Feat: build.gradle 파일에 hilt manager 관련 코드 추가
alswlekk Sep 22, 2025
7e40411
Feat: google-services.json 파일 존재 여부 상관없이 테스트 진행되도록 수정
alswlekk Sep 22, 2025
55374c0
Refactor Android CI workflow for clarity and structure
alswlekk Sep 22, 2025
590709d
Feat: 패키지명 수정
alswlekk Sep 22, 2025
a771654
Update package name in android-ci.yml
alswlekk Sep 22, 2025
4b2d693
Update API key in android-ci.yml
alswlekk Sep 22, 2025
e563638
Update google-services.json creation in CI workflow
alswlekk Sep 22, 2025
9573e20
Update CI workflow to generate google-services.json
alswlekk Sep 25, 2025
d51baa8
Merge branch 'develop' into feat/fcm-alarm-#110
alswlekk Sep 25, 2025
49cd59c
Refactor Android CI workflow for clarity and updates
alswlekk Sep 25, 2025
274adbc
Refactor Android CI workflow for google-services.json
alswlekk Sep 25, 2025
1600115
Merge branch 'develop' into feat/fcm-alarm-#110
alswlekk Sep 29, 2025
7578a24
Merge branch 'develop' of github.com:Medicare-Call/Medicare-Call-Andr…
alswlekk Oct 8, 2025
e9dd560
Refactor: FCM 관련 코드 리펙토링
alswlekk Oct 8, 2025
3cfa948
Merge branch 'develop' of github.com:Medicare-Call/Medicare-Call-Andr…
alswlekk Nov 3, 2025
9680847
Feat: SharedPreferences에 저장하는 형식에서 DataStore(직렬화)로 통일
alswlekk Nov 3, 2025
cf3449e
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
bba1831
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
4bedbef
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
37a63b7
Refactor: dataStore 이용해서 저장하는 형식으로 수정
alswlekk Nov 3, 2025
60fb502
Refactor: dataStore 이용해서 저장하는 형식으로 수정
alswlekk Nov 3, 2025
6b1f2d2
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
08129f8
Merge branch 'develop' into feat/fcm-alarm-#110
alswlekk Nov 3, 2025
0ca4014
Refactor: MainActivity 파일 수정(중복 실행 방지)
alswlekk Nov 3, 2025
26d6828
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
ff4a6b0
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
f7587db
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
32b4b43
Refactor: CI 관련 오류 코드 수정
alswlekk Nov 3, 2025
169ac56
Refactor: CI 관련 오류 코드 수정
alswlekk Nov 3, 2025
0711e97
Refactor: CI 관련 오류 코드 수정
alswlekk Nov 3, 2025
2e00557
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
b75e47e
Refactor: CI 관련 오류 코드 수정
alswlekk Nov 3, 2025
d05c31d
Refactor: CI 관련 오류 코드 수정
alswlekk Nov 3, 2025
a7908f7
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
0dbc2c3
Refactor: CI 관련 오류 코드 수정
alswlekk Nov 3, 2025
4c88cd9
Refactor: CI 관련 오류 코드 수정
alswlekk Nov 3, 2025
36b7883
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
e840883
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
c8dec6a
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 3, 2025
f94906b
Refactor: Fcm 관련 코드 기존의 토큰 관련 코드랑 독립적으로 분리
alswlekk Nov 5, 2025
4701d51
Refactor: Fcm 관련 코드 기존의 토큰 관련 코드랑 독립적으로 분리
alswlekk Nov 5, 2025
61421e1
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 5, 2025
a7e8067
Merge branch 'develop' of github.com:Medicare-Call/Medicare-Call-Andr…
alswlekk Nov 5, 2025
6ffed96
Initial plan
Copilot Nov 5, 2025
9f43a53
Refactor: CI 관련 수정사항 반영
alswlekk Nov 5, 2025
a37f941
Initial setup: Copy files from FCM branch for fixes
Copilot Nov 5, 2025
d9e79af
fix(detekt): remove extra blank line, fix FcmService indentation, rem…
Copilot Nov 5, 2025
4bb9ad1
Merge branch 'feat/fcm-alarm-#110' into copilot/fix-detekt-style-issues
alswlekk Nov 5, 2025
66cd9e9
Merge pull request #170 from Medicare-Call/copilot/fix-detekt-style-i…
alswlekk Nov 5, 2025
86c0f71
Refactor: CI 관련 수정사항 반영
alswlekk Nov 5, 2025
48ab848
Merge remote-tracking branch 'origin/feat/fcm-alarm-#110' into feat/f…
alswlekk Nov 5, 2025
cad0504
Refactor: CI 관련 수정사항 반영
alswlekk Nov 5, 2025
1ff751f
Fix: #110 잘못된 baseurl 키 변경
ProtossManse Nov 10, 2025
f3a5b46
Merge branch 'develop' of github.com:Medicare-Call/Medicare-Call-Andr…
alswlekk Nov 16, 2025
fabaa14
Fix: 서버와 FCM 관련해 로그인, 회원가입 API 연동
alswlekk Nov 18, 2025
495b2a6
Fix: ci 관련 코드 수정
alswlekk Nov 18, 2025
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
11 changes: 6 additions & 5 deletions .github/workflows/android-ci.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
name: Android CI

on:
push: # 코드 푸시 이벤트에 대한 설정
branches: [ "develop" ] # "develop" 브랜치에 푸시될 때만 트리거된다.
pull_request: # 풀 리퀘스트 이벤트에 대한 설정
branches: [ "develop" ] # "develop" 브랜치로의 풀 리퀘스트가 생성될 때만 트리거된다.
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
Comment on lines +4 to +7
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

포크 PR에서 시크릿 미제공 시 빌드 깨짐 위험 — 시크릿 의존 스텝 가드 필요

fork에서 오는 pull_request 이벤트엔 시크릿이 전달되지 않아 google-services.json/local.properties 생성 스텝이 빈 파일을 만들거나 실패 → 빌드 실패로 이어질 수 있습니다. 아래처럼 시크릿 존재 여부로 가드하세요.

추천 변경(해당 스텝 라인에서 if 추가):

-      - name: Generate local.properties
-        run: |
-          echo '${{ secrets.LOCAL_PROPERTIES }}' >> ./local.properties
+      - name: Generate local.properties
+        if: ${{ secrets.LOCAL_PROPERTIES != '' }}
+        run: |
+          printf '%s' "${{ secrets.LOCAL_PROPERTIES }}" > ./local.properties

-      - name: Generate google-services.json
-        run: |
-          echo '${{ secrets.GOOGLE_SERVICES }}' >> ./app/google-services.json
+      - name: Generate google-services.json
+        if: ${{ secrets.GOOGLE_SERVICES != '' }}
+        run: |
+          printf '%s' "${{ secrets.GOOGLE_SERVICES }}" > ./app/google-services.json

옵션(포크 PR용 스텁 파일 생성 — 시크릿 없을 때만):

- name: Create stub google-services.json for fork PRs
  if: ${{ github.event_name == 'pull_request' && secrets.GOOGLE_SERVICES == '' }}
  run: |
    printf '{ "project_info": {}, "client": [] }' > app/google-services.json

옵션(중복 러너 낭비 방지):

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
🤖 Prompt for AI Agents
.github/workflows/android-ci.yml lines 4-7: pull_request workflows from forks
don't have repository secrets, so steps that rely on GOOGLE_SERVICES or other
secrets can create empty files or fail and break the build; add guards on those
steps to only run when the secret exists (e.g., if: github.event_name ==
'pull_request' && secrets.GOOGLE_SERVICES != '') or when running on push, and
optionally add a fallback step that creates a minimal stub
google-services.json/local.properties only when the secret is missing (if:
github.event_name == 'pull_request' && secrets.GOOGLE_SERVICES == '') so fork
PRs succeed; also consider adding a concurrency block to avoid duplicate runner
usage (concurrency group and cancel-in-progress: true).


jobs: # CI에서 수행할 작업을 정의한다.
jobs:
ci-build:
runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- name: Grant execute permission for gradlew
Expand Down
52 changes: 27 additions & 25 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
alias(libs.plugins.ksp)
kotlin("plugin.serialization") version "2.0.21"
alias(libs.plugins.detekt)

alias(libs.plugins.google.services)
}

detekt {
Expand All @@ -21,11 +21,6 @@ android {
namespace = "com.konkuk.medicarecall"
compileSdk = 36

buildFeatures {
buildConfig = true
}


defaultConfig {
applicationId = "com.konkuk.medicarecall"
minSdk = 26
Expand All @@ -39,50 +34,58 @@ android {
load(project.rootProject.file("local.properties").inputStream())
}

val baseUrl = properties["base.url"]?.toString()?.let { "\"$it\"" } ?: "\"\""
buildConfigField("String", "BASE_URL", baseUrl)
buildConfigField("String", "BASE_URL", "\"${properties["base.url"] ?: ""}\"")
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11

}

kotlinOptions {
jvmTarget = "11"
}

buildFeatures {
compose = true
buildConfig = true
}

}

dependencies {


// AndroidX & Compose
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.foundation)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.runtime.android)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.datastore)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.media3.common.ktx)
implementation(libs.play.services.vision.common)

// DataStore (명시적으로 추가)
implementation("androidx.datastore:datastore-preferences:1.1.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")

// Test
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand All @@ -91,32 +94,31 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)



implementation(libs.androidx.foundation)
// kotlin serialization
// Kotlin Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")

// Retrofit
// Retrofit & OkHttp
implementation("com.squareup.retrofit2:retrofit:2.12.0")
implementation("com.squareup.retrofit2:converter-kotlinx-serialization:2.12.0")

// OkHttp
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")

// WebView
implementation("com.google.accompanist:accompanist-webview:0.24.13-rc")
detektPlugins(libs.detekt.formatting)


// Hilt
implementation(libs.hilt.android)
implementation(libs.hilt.core)
implementation(libs.hilt.navigation.compose)
ksp(libs.hilt.android.compiler)
ksp(libs.hilt.compiler)

// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.google.firebase.analytics)
implementation(libs.firebase.messaging)

// Detekt formatting plugin
detektPlugins(libs.detekt.formatting)
ksp(libs.hilt.manager)
}
Comment on lines 122 to 123
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

중요: ksp(libs.hilt.manager)는 빌드 실패 가능성이 큼(안드로이드X Hilt Compiler는 KSP 미지원)

androidx.hilt:hilt-compiler는 현재 KAPT만 지원합니다. KSP로 연결하면 빌드가 깨질 확률이 높습니다. 실제로 필요 없다면 제거, 필요하다면 KAPT로 전환하세요.

적용 diff(불필요 시 제거):

-    ksp(libs.hilt.manager)

만약 WorkManager 등의 AndroidX Hilt 확장에 컴파일러가 필요하면 다음처럼 전환:

// plugins 블록에 추가
plugins {
    id("org.jetbrains.kotlin.kapt")
}

// dependencies 블록
kapt(libs.hilt.manager)

또는 해당 확장을 쓰지 않는다면 toml의 hilt-manager 항목 자체를 삭제하세요(아래 toml 코멘트 참조).

🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 119-120, the dependency uses
ksp(libs.hilt.manager) which will likely break the build because
androidx.hilt:hilt-compiler only supports KAPT; either remove the dependency if
unused, or switch to KAPT: add the Kotlin KAPT plugin
(id("org.jetbrains.kotlin.kapt")) to the plugins block and replace
ksp(libs.hilt.manager) with kapt(libs.hilt.manager); alternatively, if you truly
don’t use the Hilt WorkManager/AndroidX extensions, delete the hilt-manager
entry from libs.versions.toml instead.


Expand Down
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />


<!-- 전화 하드웨어는 필수가 아님 (에뮬레이터/태블릿 대비) -->
<uses-feature
Expand All @@ -22,6 +24,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MediCareCall">
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="fcm_alert" />
<activity
android:name=".MainActivity"
android:exported="true"
Expand All @@ -33,6 +38,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".data.api.fcm.FcmService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>


Expand Down
86 changes: 85 additions & 1 deletion app/src/main/java/com/konkuk/medicarecall/App.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,95 @@
package com.konkuk.medicarecall

import dagger.hilt.android.HiltAndroidApp
import android.app.Application
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import android.util.Log
import com.google.firebase.messaging.FirebaseMessaging
import com.konkuk.medicarecall.data.repository.FcmRepository
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltAndroidApp
class App : Application() {
@Inject
lateinit var fcmRepository: FcmRepository

// Application 전체에서 쑬 수 있는 스코프(앱이 살아있는 동안 유지돼야 하는 초기화/저장 작업 진행 - 여러 초기화 작업 한덩어리로 관리)
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

override fun onCreate() {
super.onCreate()
createNotificationChannel()
fetchAndStoreFcmToken()
}

// Android 8.0 이상에서 FCM 알림 표시하기 위한 채널 생성
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val nm = getSystemService(NotificationManager::class.java) ?: return

val channel = NotificationChannel(
FCM_CHANNEL_ID,
"FCM 알림",
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = "Firebase Cloud Messaging으로부터 수신된 알림을 표시합니다."
enableVibration(true)
vibrationPattern = longArrayOf(0, 250, 150, 250)
setShowBadge(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}

nm.createNotificationChannel(channel)
}

/**
* 앱 시작 시점에 한 번 FCM 토큰을 가져와서 DataStore에 저장
* (실제로 토큰이 바뀌면 FirebaseMessagingService.onNewToken(...)에서도 다시 저장해야 함)
*/
private fun fetchAndStoreFcmToken() {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (!task.isSuccessful) {
if (BuildConfig.DEBUG) {
Log.w(TAG, "토큰 가져오기 실패", task.exception)
}
return@addOnCompleteListener
}

val token = task.result
if (token.isNullOrBlank()) {
if (BuildConfig.DEBUG) {
Log.w(TAG, "FCM Token is empty")
}
return@addOnCompleteListener
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "FCM token(full)=$token")
}

// FCM 토큰을 DataStore(AppPreferences)에 저장
appScope.launch {
try {
fcmRepository.saveFcmToken(token)
if (BuildConfig.DEBUG) {
Log.d(TAG, "FCM token saved to DataStore")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to save FCM token", e)
}
}
}
}

companion object {
private const val TAG = "FCM"
const val FCM_CHANNEL_ID = "fcm_alert"
}
}
44 changes: 44 additions & 0 deletions app/src/main/java/com/konkuk/medicarecall/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowInsetsController
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.WindowInsets
Expand All @@ -16,7 +19,15 @@ import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
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
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.hilt.navigation.compose.hiltViewModel
Expand Down Expand Up @@ -66,6 +77,9 @@ class MainActivity : ComponentActivity() {
val navigator = rememberMainNavigator()

MediCareCallTheme {
// 알림 권한 요청
RequestNotificationPermission()

val loginViewModel: LoginViewModel = hiltViewModel()
val loginElderViewModel: LoginElderViewModel = hiltViewModel()

Expand Down Expand Up @@ -95,3 +109,33 @@ class MainActivity : ComponentActivity() {
}
}
}

@Composable
fun RequestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val context = LocalContext.current
val permission = android.Manifest.permission.POST_NOTIFICATIONS
var hasRequested by remember { mutableStateOf(false) }

val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
) { isGranted ->
if (isGranted) {
Toast.makeText(context, "알림 권한이 허용되었습니다.", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "알림 권한이 거부되었습니다.", Toast.LENGTH_SHORT).show()
}
}

LaunchedEffect(hasRequested) {
if (!hasRequested &&
ContextCompat.checkSelfPermission(context, permission) !=
android.content.pm.PackageManager.PERMISSION_GRANTED
) {
// shouldShowRequestPermissionRationale 체크 추가 권장
hasRequested = true
launcher.launch(permission)
}
}
}
}
Loading
Loading