\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..41daf38
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 74dd639..b2c751a 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
diff --git a/app/src/main/java/com/khigh/seniormap/ui/composables/navigation/AppNavigation.kt b/app/src/main/java/com/khigh/seniormap/ui/composables/navigation/AppNavigation.kt
index 08bec45..cb8c57f 100644
--- a/app/src/main/java/com/khigh/seniormap/ui/composables/navigation/AppNavigation.kt
+++ b/app/src/main/java/com/khigh/seniormap/ui/composables/navigation/AppNavigation.kt
@@ -10,9 +10,14 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
import com.khigh.seniormap.ui.screens.HomeScreen
import com.khigh.seniormap.ui.screens.LoadingScreen
import com.khigh.seniormap.ui.screens.LoginScreen
+import com.khigh.seniormap.ui.screens.guardian.GuardianHomeScreen
+import com.khigh.seniormap.ui.screens.guardian.EditGuardianScreen
+import com.khigh.seniormap.ui.screens.guardian.AddProtectedPersonScreen
import com.khigh.seniormap.viewmodel.AuthViewModel
import androidx.hilt.navigation.compose.hiltViewModel
@@ -56,20 +61,82 @@ fun AppNavigation(
Log.d("com.khigh.seniormap", "[AppNavigation] isAuthenticated: $isAuthenticated, authState: $authState")
NavHost(
navController = navController,
- startDestination = if (isAuthenticated) "home" else "login"
+ startDestination = if (isAuthenticated) "guardian_home" else "login"
) {
// 로그인 화면
composable("login") {
LoginScreen(
onNavigateToHome = {
- navController.navigate("home") {
+ navController.navigate("guardian_home") {
popUpTo("login") { inclusive = true }
}
}
)
}
- // 홈 화면
+ // 보호인 홈 화면
+ composable("guardian_home") {
+ GuardianHomeScreen(
+ onNavigateToLogin = {
+ navController.navigate("login") {
+ popUpTo("guardian_home") { inclusive = true }
+ }
+ },
+ onNavigateToEdit = { guardianData ->
+ navController.navigate("edit_guardian/${guardianData.id}") {
+ // 뒤로 가기 시 홈 화면으로 돌아감
+ }
+ },
+ onNavigateToAdd = {
+ navController.navigate("add_protected_person")
+ }
+ )
+ }
+
+ // 피보호인 추가 화면
+ composable("add_protected_person") {
+ AddProtectedPersonScreen(
+ onNavigateBack = {
+ navController.popBackStack()
+ },
+ onSave = { protectedPersonData ->
+ // TODO: 실제로는 ViewModel을 통해 데이터를 저장해야 함
+ Log.d("AppNavigation", "Protected person added: $protectedPersonData")
+ navController.popBackStack()
+ }
+ )
+ }
+
+ // 피보호인 수정 화면
+ composable(
+ route = "edit_guardian/{guardianId}",
+ arguments = listOf(
+ navArgument("guardianId") { type = NavType.StringType }
+ )
+ ) { backStackEntry ->
+ val guardianId = backStackEntry.arguments?.getString("guardianId") ?: ""
+ // TODO: 실제로는 ViewModel에서 guardianId로 데이터를 가져와야 함
+ val guardianData = com.khigh.seniormap.ui.screens.guardian.components.GuardianData(
+ id = guardianId,
+ name = "임시 피보호인",
+ location = "임시 위치",
+ isAtHome = true
+ )
+
+ EditGuardianScreen(
+ guardianData = guardianData,
+ onNavigateBack = {
+ navController.popBackStack()
+ },
+ onSave = { updatedGuardian ->
+ // TODO: 실제로는 ViewModel을 통해 데이터를 저장해야 함
+ Log.d("AppNavigation", "Guardian updated: $updatedGuardian")
+ navController.popBackStack()
+ }
+ )
+ }
+
+ // 기존 홈 화면 (필요시 유지)
composable("home") {
HomeScreen(
onNavigateToLogin = {
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/LocationUtils.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/LocationUtils.kt
new file mode 100644
index 0000000..7c881c1
--- /dev/null
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/LocationUtils.kt
@@ -0,0 +1,82 @@
+package com.khigh.seniormap.ui.screens
+
+import kotlin.math.*
+
+/**
+ * 위치 관련 유틸리티 클래스
+ *
+ * 지리적 좌표 간의 거리 계산과 위치 상태 판단을 담당합니다.
+ */
+object LocationUtils {
+
+ /**
+ * 두 지점 간의 거리를 미터 단위로 계산합니다.
+ * Haversine 공식을 사용하여 지구의 곡률을 고려합니다.
+ *
+ * @param lat1 첫 번째 지점의 위도 (도)
+ * @param lon1 첫 번째 지점의 경도 (도)
+ * @param lat2 두 번째 지점의 위도 (도)
+ * @param lon2 두 번째 지점의 경도 (도)
+ * @return 두 지점 간의 거리 (미터)
+ */
+ fun calculateDistance(
+ lat1: Double, lon1: Double,
+ lat2: Double, lon2: Double
+ ): Double {
+ val earthRadius = 6371000.0 // 지구 반지름 (미터)
+
+ val dLat = Math.toRadians(lat2 - lat1)
+ val dLon = Math.toRadians(lon2 - lon1)
+
+ val a = sin(dLat / 2) * sin(dLat / 2) +
+ cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
+ sin(dLon / 2) * sin(dLon / 2)
+
+ val c = 2 * atan2(sqrt(a), sqrt(1 - a))
+
+ return earthRadius * c
+ }
+
+ /**
+ * 현재 위치가 집에 있는지 여부를 판단합니다.
+ *
+ * @param homeLat 집의 위도 (도)
+ * @param homeLon 집의 경도 (도)
+ * @param currentLat 현재 위치의 위도 (도)
+ * @param currentLon 현재 위치의 경도 (도)
+ * @param threshold 집으로 간주하는 거리 임계값 (미터, 기본값: 100m)
+ * @return 집에 있으면 true, 외출 중이면 false
+ */
+ fun isAtHome(
+ homeLat: Double, homeLon: Double,
+ currentLat: Double, currentLon: Double,
+ threshold: Double = 100.0
+ ): Boolean {
+ val distance = calculateDistance(homeLat, homeLon, currentLat, currentLon)
+ return distance <= threshold
+ }
+
+ /**
+ * 거리를 사람이 읽기 쉬운 형태로 변환합니다.
+ *
+ * @param distanceMeters 거리 (미터)
+ * @return 포맷된 거리 문자열
+ */
+ fun formatDistance(distanceMeters: Double): String {
+ return when {
+ distanceMeters < 1000 -> "${distanceMeters.toInt()}m"
+ else -> "${(distanceMeters / 1000).roundToInt()}km"
+ }
+ }
+
+ /**
+ * 위도와 경도가 유효한지 확인합니다.
+ *
+ * @param latitude 위도
+ * @param longitude 경도
+ * @return 유효한 좌표이면 true
+ */
+ fun isValidCoordinate(latitude: Double, longitude: Double): Boolean {
+ return latitude in -90.0..90.0 && longitude in -180.0..180.0
+ }
+}
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/LoginScreen.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/LoginScreen.kt
index 4f11ba7..e7e595f 100644
--- a/app/src/main/java/com/khigh/seniormap/ui/screens/LoginScreen.kt
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/LoginScreen.kt
@@ -124,6 +124,10 @@ fun LoginScreen(
onGoogleLogin = {
Log.d("com.khigh.seniormap", "[LoginScreen] Google login clicked")
authViewModel.loginWithOAuth(Google)
+ },
+ onTemporaryGuardianHome = {
+ Log.d("com.khigh.seniormap", "[LoginScreen] Temporary guardian home clicked")
+ onNavigateToHome()
}
)
}
@@ -163,6 +167,7 @@ fun LoginScreen(
private fun LoginButtonsSection(
onKakaoLogin: () -> Unit,
onGoogleLogin: () -> Unit,
+ onTemporaryGuardianHome: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
@@ -214,5 +219,18 @@ private fun LoginButtonsSection(
fontWeight = FontWeight.Medium
)
}
+
+ // 임시 보호인 홈 이동 버튼
+ Spacer(modifier = Modifier.height(24.dp))
+ TextButton(
+ onClick = onTemporaryGuardianHome,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ text = "(임시) 보호인 홈 넘어가기",
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/AddProtectedPersonScreen.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/AddProtectedPersonScreen.kt
new file mode 100644
index 0000000..e1bdd88
--- /dev/null
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/AddProtectedPersonScreen.kt
@@ -0,0 +1,415 @@
+package com.khigh.seniormap.ui.screens.guardian
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+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.text.font.FontWeight
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+/**
+ * 피보호인 추가 화면
+ *
+ * 새로운 피보호인을 등록할 수 있는 화면입니다.
+ * 프로필 사진, 기본 정보, 비상 연락처, 추가 정보를 입력받습니다.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AddProtectedPersonScreen(
+ modifier: Modifier = Modifier,
+ onNavigateBack: () -> Unit = {},
+ onSave: (ProtectedPersonData) -> Unit = {}
+) {
+ // 입력 데이터 상태
+ var name by remember { mutableStateOf(TextFieldValue("")) }
+ var phoneNumber by remember { mutableStateOf(TextFieldValue("")) }
+ var homePhone by remember { mutableStateOf(TextFieldValue("")) }
+ var address by remember { mutableStateOf(TextFieldValue("")) }
+ var emergencyContact by remember { mutableStateOf(TextFieldValue("")) }
+ var relationship by remember { mutableStateOf(TextFieldValue("")) }
+ var notes by remember { mutableStateOf(TextFieldValue("")) }
+
+ // 프로필 사진 상태
+ var hasProfileImage by remember { mutableStateOf(false) }
+
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = "피보호인 추가",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(
+ imageVector = Icons.Default.ArrowBack,
+ contentDescription = "뒤로 가기"
+ )
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = Color.Transparent
+ )
+ )
+ }
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = 16.dp)
+ ) {
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // 프로필 사진 섹션
+ ProfileImageSection(
+ hasImage = hasProfileImage,
+ onAddPhoto = {
+ // TODO: 카메라/갤러리에서 사진 선택 구현
+ hasProfileImage = true
+ }
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ // 기본 정보 섹션
+ InputField(
+ label = "성함",
+ value = name,
+ onValueChange = { name = it },
+ placeholder = "입력"
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ InputField(
+ label = "전화번호",
+ value = phoneNumber,
+ onValueChange = { phoneNumber = it },
+ placeholder = "입력"
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ InputField(
+ label = "집전화",
+ value = homePhone,
+ onValueChange = { homePhone = it },
+ placeholder = "입력"
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ InputField(
+ label = "집 주소",
+ value = address,
+ onValueChange = { address = it },
+ placeholder = "검색",
+ isSearchable = true
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ // 비상 연락처 섹션
+ SectionHeader(title = "비상 연락처")
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ InputField(
+ label = "비상 연락처 번호",
+ value = emergencyContact,
+ onValueChange = { emergencyContact = it },
+ placeholder = "입력"
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ InputField(
+ label = "관계",
+ value = relationship,
+ onValueChange = { relationship = it },
+ placeholder = "입력"
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ // 추가 정보 섹션
+ SectionHeader(title = "추가 정보")
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ NotesInputField(
+ label = "비고",
+ value = notes,
+ onValueChange = { notes = it },
+ placeholder = "입력"
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ // 승인 요청 버튼
+ Button(
+ onClick = {
+ val protectedPersonData = ProtectedPersonData(
+ name = name.text,
+ phoneNumber = phoneNumber.text,
+ homePhone = homePhone.text,
+ address = address.text,
+ emergencyContact = emergencyContact.text,
+ relationship = relationship.text,
+ notes = notes.text
+ )
+ onSave(protectedPersonData)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color(0xFFFFD700) // 밝은 노란색
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Text(
+ text = "승인 요청",
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black
+ )
+ }
+
+ Spacer(modifier = Modifier.height(32.dp))
+ }
+ }
+}
+
+/**
+ * 프로필 사진 섹션
+ */
+@Composable
+private fun ProfileImageSection(
+ hasImage: Boolean,
+ onAddPhoto: () -> Unit
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // 프로필 이미지
+ Box(
+ modifier = Modifier
+ .size(120.dp)
+ .clip(CircleShape)
+ .background(
+ if (hasImage) Color(0xFFE0E0E0) else Color(0xFFF5F5F5)
+ )
+ .border(
+ width = 2.dp,
+ color = if (hasImage) Color(0xFFE0E0E0) else Color(0xFFCCCCCC),
+ shape = CircleShape
+ )
+ .clickable { onAddPhoto() },
+ contentAlignment = Alignment.Center
+ ) {
+ if (hasImage) {
+ // TODO: 실제 이미지 표시
+ Icon(
+ imageVector = Icons.Default.Person,
+ contentDescription = "프로필 사진",
+ modifier = Modifier.size(48.dp),
+ tint = Color.Gray
+ )
+ } else {
+ Icon(
+ imageVector = Icons.Default.Add,
+ contentDescription = "사진 추가",
+ modifier = Modifier.size(48.dp),
+ tint = Color.Gray
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = "+ 사진 추가",
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = "빠른 식별을 위한 사진을 추가하세요",
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color.Gray,
+ textAlign = TextAlign.Center
+ )
+ }
+}
+
+/**
+ * 입력 필드 컴포넌트
+ */
+@Composable
+private fun InputField(
+ label: String,
+ value: TextFieldValue,
+ onValueChange: (TextFieldValue) -> Unit,
+ placeholder: String,
+ isSearchable: Boolean = false
+) {
+ Column {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp)
+ .background(
+ color = Color(0xFFF8F8F8),
+ shape = RoundedCornerShape(8.dp)
+ )
+ .border(
+ width = 1.dp,
+ color = Color(0xFFE0E0E0),
+ shape = RoundedCornerShape(8.dp)
+ )
+ .clickable { if (isSearchable) { /* TODO: 주소 검색 화면 열기 */ } }
+ ) {
+ BasicTextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp),
+ textStyle = MaterialTheme.typography.bodyLarge.copy(
+ color = if (value.text.isEmpty()) Color.Gray else Color.Black
+ ),
+ decorationBox = { innerTextField ->
+ if (value.text.isEmpty()) {
+ Text(
+ text = placeholder,
+ style = MaterialTheme.typography.bodyLarge,
+ color = Color.Gray
+ )
+ }
+ innerTextField()
+ }
+ )
+ }
+ }
+}
+
+/**
+ * 비고 입력 필드 (여러 줄)
+ */
+@Composable
+private fun NotesInputField(
+ label: String,
+ value: TextFieldValue,
+ onValueChange: (TextFieldValue) -> Unit,
+ placeholder: String
+) {
+ Column {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(120.dp)
+ .background(
+ color = Color(0xFFF8F8F8),
+ shape = RoundedCornerShape(8.dp)
+ )
+ .border(
+ width = 1.dp,
+ color = Color(0xFFE0E0E0),
+ shape = RoundedCornerShape(8.dp)
+ )
+ ) {
+ BasicTextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ textStyle = MaterialTheme.typography.bodyLarge.copy(
+ color = if (value.text.isEmpty()) Color.Gray else Color.Black
+ ),
+ decorationBox = { innerTextField ->
+ if (value.text.isEmpty()) {
+ Text(
+ text = placeholder,
+ style = MaterialTheme.typography.bodyLarge,
+ color = Color.Gray
+ )
+ }
+ innerTextField()
+ }
+ )
+ }
+ }
+}
+
+/**
+ * 섹션 헤더
+ */
+@Composable
+private fun SectionHeader(title: String) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black
+ )
+}
+
+/**
+ * 피보호인 데이터 모델
+ */
+data class ProtectedPersonData(
+ val name: String = "",
+ val phoneNumber: String = "",
+ val homePhone: String = "",
+ val address: String = "",
+ val emergencyContact: String = "",
+ val relationship: String = "",
+ val notes: String = ""
+)
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/EditGuardianScreen.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/EditGuardianScreen.kt
new file mode 100644
index 0000000..bafcc42
--- /dev/null
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/EditGuardianScreen.kt
@@ -0,0 +1,261 @@
+package com.khigh.seniormap.ui.screens.guardian
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+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.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import com.khigh.seniormap.ui.screens.guardian.components.GuardianData
+
+/**
+ * 피보호인 정보 수정 화면
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun EditGuardianScreen(
+ guardianData: GuardianData,
+ onNavigateBack: () -> Unit = {},
+ onSave: (GuardianData) -> Unit = {},
+ modifier: Modifier = Modifier
+) {
+ // 수정할 데이터 상태
+ var name by remember { mutableStateOf(guardianData.name) }
+ var location by remember { mutableStateOf(guardianData.location) }
+ var phoneNumber by remember { mutableStateOf("010-1234-5678") } // 임시 데이터
+ var emergencyContact by remember { mutableStateOf("010-9876-5432") } // 임시 데이터
+ var address by remember { mutableStateOf("서울시 강남구 테헤란로 123") } // 임시 데이터
+ var notes by remember { mutableStateOf("특별한 주의사항이 있습니다.") } // 임시 데이터
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("피보호인 수정") },
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(
+ imageVector = Icons.Default.ArrowBack,
+ contentDescription = "뒤로 가기"
+ )
+ }
+ },
+ actions = {
+ TextButton(
+ onClick = {
+ // 수정된 데이터로 저장
+ val updatedGuardian = guardianData.copy(
+ name = name,
+ location = location
+ )
+ onSave(updatedGuardian)
+ onNavigateBack()
+ }
+ ) {
+ Text(
+ text = "저장",
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ // 프로필 이미지 섹션
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .clip(CircleShape)
+ .clickable { /* TODO: 이미지 선택 */ },
+ contentAlignment = Alignment.Center
+ ) {
+ if (guardianData.profileImageRes != null) {
+ Image(
+ painter = painterResource(id = guardianData.profileImageRes),
+ contentDescription = "프로필 이미지",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ } else {
+ // 기본 프로필 아이콘
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = "프로필 이미지 선택",
+ modifier = Modifier
+ .size(40.dp)
+ .padding(20.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = "프로필 이미지 선택",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+
+ // 기본 정보 섹션
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ text = "기본 정보",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+
+ EditField(
+ label = "이름",
+ value = name,
+ onValueChange = { name = it }
+ )
+
+ EditField(
+ label = "현재 위치",
+ value = location,
+ onValueChange = { location = it }
+ )
+
+ EditField(
+ label = "연락처",
+ value = phoneNumber,
+ keyboardType = KeyboardType.Phone,
+ onValueChange = { phoneNumber = it }
+ )
+ }
+ }
+
+ // 비상 연락처 섹션
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ text = "비상 연락처",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+
+ EditField(
+ label = "비상 연락처",
+ value = emergencyContact,
+ keyboardType = KeyboardType.Phone,
+ onValueChange = { emergencyContact = it }
+ )
+
+ EditField(
+ label = "주소",
+ value = address,
+ onValueChange = { address = it }
+ )
+ }
+ }
+
+ // 기타 정보 섹션
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ text = "기타 정보",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+
+ EditField(
+ label = "특이사항",
+ value = notes,
+ onValueChange = { notes = it }
+ )
+ }
+ }
+
+ // 하단 여백
+ Spacer(modifier = Modifier.height(32.dp))
+ }
+ }
+}
+
+/**
+ * 편집 가능한 입력 필드 컴포넌트
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun EditField(
+ label: String,
+ value: String,
+ keyboardType: KeyboardType = KeyboardType.Text,
+ onValueChange: (String) -> Unit
+) {
+ OutlinedTextField(
+ value = value,
+ onValueChange = onValueChange,
+ label = { Text(label) },
+ modifier = Modifier.fillMaxWidth(),
+ keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
+ keyboardType = keyboardType
+ ),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
+ unfocusedBorderColor = MaterialTheme.colorScheme.outline
+ )
+ )
+}
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/GuardianHomeScreen.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/GuardianHomeScreen.kt
new file mode 100644
index 0000000..499478c
--- /dev/null
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/GuardianHomeScreen.kt
@@ -0,0 +1,157 @@
+package com.khigh.seniormap.ui.screens.guardian
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.khigh.seniormap.ui.screens.guardian.components.*
+
+/**
+ * 보호인 홈 화면 컴포넌트
+ *
+ * 보호인이 피보호인들의 현재 상태와 위치를 확인할 수 있는 메인 화면입니다.
+ * 피보호인 목록과 각각의 상태 정보를 표시하며, 하단 네비게이션을 통해
+ * 다른 기능들로 이동할 수 있습니다.
+ *
+ * @param modifier 레이아웃 수정자
+ * @param onNavigateToLogin 로그인 화면으로 이동하는 콜백
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun GuardianHomeScreen(
+ modifier: Modifier = Modifier,
+ onNavigateToLogin: () -> Unit = {},
+ onNavigateToEdit: (GuardianData) -> Unit = {},
+ onNavigateToAdd: () -> Unit = {} // 추가된 콜백
+) {
+ // 수정 모드 상태 추가
+ var isEditMode by remember { mutableStateOf(false) }
+ // 임시 데이터 - 실제로는 ViewModel에서 가져올 데이터 (위치 정보 포함)
+ val guardians = remember {
+ listOf(
+ GuardianData(
+ id = "1",
+ name = "김할자",
+ location = "집",
+ isAtHome = true,
+ homeAddress = "서울특별시 강남구 테헤란로 123",
+ homeLatitude = 37.5665,
+ homeLongitude = 126.9780,
+ currentLatitude = 37.5665,
+ currentLongitude = 126.9780
+ ),
+ GuardianData(
+ id = "2",
+ name = "송진호",
+ location = "마을",
+ isAtHome = false,
+ homeAddress = "서울특별시 서초구 서초대로 456",
+ homeLatitude = 37.5013,
+ homeLongitude = 127.0246,
+ currentLatitude = 37.5020,
+ currentLongitude = 127.0250
+ ),
+ GuardianData(
+ id = "3",
+ name = "나문희",
+ location = "집",
+ isAtHome = true,
+ homeAddress = "서울특별시 마포구 와우산로 789",
+ homeLatitude = 37.5519,
+ homeLongitude = 126.9251,
+ currentLatitude = 37.5519,
+ currentLongitude = 126.9251
+ ),
+ GuardianData(
+ id = "4",
+ name = "최불암",
+ location = "마을",
+ isAtHome = false,
+ homeAddress = "서울특별시 종로구 종로 101",
+ homeLatitude = 37.5736,
+ homeLongitude = 126.9787,
+ currentLatitude = 37.5750,
+ currentLongitude = 126.9800
+ )
+ )
+ }
+
+ // 현재 선택된 하단 네비게이션 탭
+ var selectedTab by remember { mutableIntStateOf(0) }
+
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ // 수정된 헤더 (수정 버튼 추가)
+ GuardianHeader(
+ title = "나의 피보호인",
+ isEditMode = isEditMode,
+ onEditClick = {
+ isEditMode = !isEditMode // 수정 모드 토글
+ },
+ onAddClick = {
+ onNavigateToAdd() // 피보호인 추가 화면으로 이동
+ }
+ )
+ },
+ bottomBar = {
+ // 하단 네비게이션 바
+ BottomNavigationBar(
+ selectedTab = selectedTab,
+ onTabSelected = { tabIndex ->
+ selectedTab = tabIndex
+ // TODO: 각 탭에 따른 화면 전환 로직
+ }
+ )
+ }
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ when (selectedTab) {
+ 0 -> {
+ // 홈 탭 - 피보호인 목록 (수정 모드 전달)
+ GuardianList(
+ guardians = guardians,
+ isEditMode = isEditMode,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 8.dp),
+ onGuardianClick = { guardian ->
+ // 일반 모드에서만 상세 화면 이동
+ if (!isEditMode) {
+ // TODO: 피보호인 상세 화면으로 이동
+ }
+ },
+ onGuardianEdit = { guardian ->
+ // 수정 화면으로 이동
+ onNavigateToEdit(guardian)
+ }
+ )
+ }
+ 1 -> {
+ // 지도 탭 - 지도 화면
+ MapScreen(
+ guardians = guardians,
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ 2 -> {
+ // 설정 탭 - 설정 화면 (임시)
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = androidx.compose.ui.Alignment.Center
+ ) {
+ Text(
+ text = "설정 화면 (구현 예정)",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/MapScreen.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/MapScreen.kt
new file mode 100644
index 0000000..2b51548
--- /dev/null
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/MapScreen.kt
@@ -0,0 +1,257 @@
+package com.khigh.seniormap.ui.screens.guardian
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+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.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.khigh.seniormap.ui.screens.guardian.components.GuardianData
+import com.khigh.seniormap.ui.screens.LocationUtils
+
+/**
+ * 지도 화면 컴포넌트
+ *
+ * 피보호인들의 현재 위치와 상태를 지도와 함께 표시합니다.
+ * 현재는 지도 부분을 빈칸으로 두고 위치 정보만 표시합니다.
+ *
+ * @param guardians 피보호인 목록 데이터
+ * @param modifier 레이아웃 수정자
+ */
+@Composable
+fun MapScreen(
+ guardians: List,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // 검색바
+ SearchBar()
+
+ // 지도 영역 (현재는 빈칸)
+ MapPlaceholder()
+
+ // 피보호인 위치 정보
+ ProtectedPersonLocationList(guardians = guardians)
+ }
+}
+
+/**
+ * 검색바 컴포넌트
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SearchBar() {
+ var searchText by remember { mutableStateOf("") }
+
+ OutlinedTextField(
+ value = searchText,
+ onValueChange = { searchText = it },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ placeholder = {
+ Text("주소를 검색하세요")
+ },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = "검색"
+ )
+ },
+ trailingIcon = {
+ Icon(
+ imageVector = Icons.Default.LocationOn,
+ contentDescription = "현재 위치"
+ )
+ },
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
+ unfocusedBorderColor = MaterialTheme.colorScheme.outline
+ ),
+ shape = RoundedCornerShape(12.dp)
+ )
+}
+
+/**
+ * 지도 플레이스홀더 (현재는 빈칸)
+ */
+@Composable
+private fun MapPlaceholder() {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp)
+ .padding(horizontal = 16.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(MaterialTheme.colorScheme.surfaceVariant),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.LocationOn,
+ contentDescription = "지도",
+ modifier = Modifier.size(48.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ Text(
+ text = "지도가 여기에 표시됩니다",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ Text(
+ text = "지도 구현 예정",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
+ )
+ }
+ }
+}
+
+/**
+ * 피보호인 위치 정보 목록
+ */
+@Composable
+private fun ProtectedPersonLocationList(guardians: List) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ Text(
+ text = "피보호인 위치",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ contentPadding = PaddingValues(vertical = 8.dp)
+ ) {
+ items(guardians) { guardian ->
+ ProtectedPersonLocationCard(guardian = guardian)
+ }
+ }
+ }
+}
+
+/**
+ * 피보호인 위치 정보 카드
+ */
+@Composable
+private fun ProtectedPersonLocationCard(guardian: GuardianData) {
+ // 현재 위치가 있는 경우 거리 계산, 없으면 기본값
+ val distance = if (guardian.currentLatitude != null && guardian.currentLongitude != null) {
+ LocationUtils.calculateDistance(
+ guardian.homeLatitude, guardian.homeLongitude,
+ guardian.currentLatitude, guardian.currentLongitude
+ )
+ } else {
+ 0.0
+ }
+
+ // 집에 있는지 여부 판단
+ val isAtHome = if (guardian.currentLatitude != null && guardian.currentLongitude != null) {
+ LocationUtils.isAtHome(
+ guardian.homeLatitude, guardian.homeLongitude,
+ guardian.currentLatitude, guardian.currentLongitude
+ )
+ } else {
+ guardian.isAtHome // 기존 데이터 사용
+ }
+
+ Card(
+ modifier = Modifier.width(160.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // 프로필 이미지 (현재는 기본 아이콘)
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .clip(RoundedCornerShape(24.dp))
+ .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.LocationOn,
+ contentDescription = "프로필",
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // 이름
+ Text(
+ text = guardian.name,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ // 상태 (집/외출)
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.LocationOn,
+ contentDescription = "위치",
+ modifier = Modifier.size(16.dp),
+ tint = if (isAtHome) Color(0xFF4CAF50) else Color(0xFFFF9800)
+ )
+
+ Text(
+ text = if (isAtHome) "집" else "외출",
+ style = MaterialTheme.typography.bodyMedium,
+ color = if (isAtHome) Color(0xFF4CAF50) else Color(0xFFFF9800),
+ fontWeight = FontWeight.Medium
+ )
+ }
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ // 거리 정보
+ if (guardian.currentLatitude != null && guardian.currentLongitude != null) {
+ Text(
+ text = "집으로부터 ${LocationUtils.formatDistance(distance)}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ } else {
+ Text(
+ text = "위치 정보 없음",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/BottomNavigationBar.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/BottomNavigationBar.kt
new file mode 100644
index 0000000..cae263e
--- /dev/null
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/BottomNavigationBar.kt
@@ -0,0 +1,114 @@
+package com.khigh.seniormap.ui.screens.guardian.components
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.outlined.Home
+import androidx.compose.material.icons.outlined.LocationOn
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+/**
+ * 하단 네비게이션 바 컴포넌트
+ *
+ * @param selectedTab 현재 선택된 탭
+ * @param modifier 레이아웃 수정자
+ * @param onTabSelected 탭 선택 이벤트 콜백
+ */
+@Composable
+fun BottomNavigationBar(
+ selectedTab: Int = 0,
+ modifier: Modifier = Modifier,
+ onTabSelected: (Int) -> Unit = {}
+) {
+ NavigationBar(
+ modifier = modifier,
+ containerColor = MaterialTheme.colorScheme.surface,
+ contentColor = MaterialTheme.colorScheme.onSurface
+ ) {
+ // 홈 탭
+ NavigationBarItem(
+ icon = {
+ Icon(
+ imageVector = if (selectedTab == 0) Icons.Filled.Home else Icons.Outlined.Home,
+ contentDescription = "홈"
+ )
+ },
+ label = {
+ Text(
+ text = "홈",
+ style = MaterialTheme.typography.labelSmall,
+ fontWeight = if (selectedTab == 0) FontWeight.Bold else FontWeight.Normal
+ )
+ },
+ selected = selectedTab == 0,
+ onClick = { onTabSelected(0) },
+ colors = NavigationBarItemDefaults.colors(
+ selectedIconColor = MaterialTheme.colorScheme.primary,
+ selectedTextColor = MaterialTheme.colorScheme.primary,
+ unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ indicatorColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
+ )
+ )
+
+ // 지도 탭
+ NavigationBarItem(
+ icon = {
+ Icon(
+ imageVector = if (selectedTab == 1) Icons.Filled.LocationOn else Icons.Outlined.LocationOn,
+ contentDescription = "지도"
+ )
+ },
+ label = {
+ Text(
+ text = "지도",
+ style = MaterialTheme.typography.labelSmall,
+ fontWeight = if (selectedTab == 1) FontWeight.Bold else FontWeight.Normal
+ )
+ },
+ selected = selectedTab == 1,
+ onClick = { onTabSelected(1) },
+ colors = NavigationBarItemDefaults.colors(
+ selectedIconColor = MaterialTheme.colorScheme.primary,
+ selectedTextColor = MaterialTheme.colorScheme.primary,
+ unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ indicatorColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
+ )
+ )
+
+ // 설정 탭
+ NavigationBarItem(
+ icon = {
+ Icon(
+ imageVector = if (selectedTab == 2) Icons.Filled.Settings else Icons.Outlined.Settings,
+ contentDescription = "설정"
+ )
+ },
+ label = {
+ Text(
+ text = "설정",
+ style = MaterialTheme.typography.labelSmall,
+ fontWeight = if (selectedTab == 2) FontWeight.Bold else FontWeight.Normal
+ )
+ },
+ selected = selectedTab == 2,
+ onClick = { onTabSelected(2) },
+ colors = NavigationBarItemDefaults.colors(
+ selectedIconColor = MaterialTheme.colorScheme.primary,
+ selectedTextColor = MaterialTheme.colorScheme.primary,
+ unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ indicatorColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianData.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianData.kt
new file mode 100644
index 0000000..40b8818
--- /dev/null
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianData.kt
@@ -0,0 +1,18 @@
+package com.khigh.seniormap.ui.screens.guardian.components
+
+/**
+ * 보호인 목록 데이터 클래스
+ */
+data class GuardianData(
+ val id: String,
+ val name: String,
+ val location: String,
+ val profileImageRes: Int? = null,
+ val isAtHome: Boolean = true,
+ // 위치 관련 필드 추가
+ val homeAddress: String = "",
+ val homeLatitude: Double = 0.0,
+ val homeLongitude: Double = 0.0,
+ val currentLatitude: Double? = null,
+ val currentLongitude: Double? = null
+)
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianHeader.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianHeader.kt
new file mode 100644
index 0000000..947da3a
--- /dev/null
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianHeader.kt
@@ -0,0 +1,83 @@
+package com.khigh.seniormap.ui.screens.guardian.components
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+/**
+ * 보호인 홈 화면 헤더 컴포넌트
+ *
+ * @param title 헤더 제목
+ * @param isEditMode 수정 모드 여부
+ * @param modifier 레이아웃 수정자
+ * @param onEditClick 수정 버튼 클릭 이벤트 콜백
+ * @param onAddClick 추가 버튼 클릭 이벤트 콜백
+ */
+@Composable
+fun GuardianHeader(
+ title: String = "나의 피보호인",
+ isEditMode: Boolean = false,
+ modifier: Modifier = Modifier,
+ onEditClick: () -> Unit = {},
+ onAddClick: () -> Unit = {}
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 제목
+ Text(
+ text = title,
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ // 수정 버튼과 추가 버튼
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 수정 버튼
+ TextButton(
+ onClick = onEditClick,
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = if (isEditMode)
+ MaterialTheme.colorScheme.primary
+ else
+ MaterialTheme.colorScheme.onSurface
+ )
+ ) {
+ Text(
+ text = if (isEditMode) "완료" else "수정",
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium
+ )
+ }
+
+ // 추가 버튼
+ IconButton(
+ onClick = onAddClick,
+ colors = IconButtonDefaults.iconButtonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ )
+ ) {
+ Icon(
+ imageVector = Icons.Default.Add,
+ contentDescription = "피보호인 추가",
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianList.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianList.kt
new file mode 100644
index 0000000..f1877c1
--- /dev/null
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianList.kt
@@ -0,0 +1,49 @@
+package com.khigh.seniormap.ui.screens.guardian.components
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+/**
+ * 보호인 목록 컴포넌트
+ *
+ * @param guardians 보호인 목록 데이터
+ * @param isEditMode 수정 모드 여부
+ * @param modifier 레이아웃 수정자
+ * @param onGuardianClick 보호인 클릭 이벤트 콜백
+ * @param onGuardianEdit 보호인 수정 이벤트 콜백
+ */
+@Composable
+fun GuardianList(
+ guardians: List,
+ isEditMode: Boolean = false,
+ modifier: Modifier = Modifier,
+ onGuardianClick: (GuardianData) -> Unit = {},
+ onGuardianEdit: (GuardianData) -> Unit = {}
+) {
+ LazyColumn(
+ modifier = modifier,
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(guardians) { guardian ->
+ GuardianListItem(
+ name = guardian.name,
+ location = guardian.location,
+ profileImageRes = guardian.profileImageRes,
+ statusIcon = if (guardian.isAtHome) Icons.Default.Home else Icons.Default.LocationOn,
+ statusColor = if (guardian.isAtHome) Color(0xFF4CAF50) else Color(0xFFFF9800),
+ isEditMode = isEditMode,
+ onClick = { onGuardianClick(guardian) },
+ onEditClick = { onGuardianEdit(guardian) }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianListItem.kt b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianListItem.kt
new file mode 100644
index 0000000..752fc6a
--- /dev/null
+++ b/app/src/main/java/com/khigh/seniormap/ui/screens/guardian/components/GuardianListItem.kt
@@ -0,0 +1,143 @@
+package com.khigh.seniormap.ui.screens.guardian.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+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.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+/**
+ * 보호인 목록 아이템 컴포넌트
+ *
+ * @param name 보호인 이름
+ * @param location 현재 위치
+ * @param profileImageRes 프로필 이미지 리소스 ID (null이면 기본 아이콘 사용)
+ * @param statusIcon 상태 아이콘
+ * @param statusColor 상태 색상
+ * @param isEditMode 수정 모드 여부
+ * @param modifier 레이아웃 수정자
+ * @param onClick 클릭 이벤트 콜백
+ * @param onEditClick 수정 버튼 클릭 이벤트 콜백
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun GuardianListItem(
+ name: String,
+ location: String,
+ profileImageRes: Int? = null,
+ statusIcon: ImageVector = Icons.Default.Home,
+ statusColor: Color = Color(0xFF4CAF50),
+ isEditMode: Boolean = false,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit = {},
+ onEditClick: () -> Unit = {}
+) {
+ Card(
+ onClick = if (!isEditMode) onClick else { {} },
+ modifier = modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 2.dp
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 프로필 이미지 또는 기본 아이콘
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .clip(CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ if (profileImageRes != null) {
+ Image(
+ painter = painterResource(id = profileImageRes),
+ contentDescription = "$name 프로필",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ } else {
+ // 기본 프로필 아이콘
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
+ ) {
+ Icon(
+ imageVector = Icons.Default.LocationOn,
+ contentDescription = "기본 프로필",
+ modifier = Modifier
+ .size(24.dp)
+ .padding(12.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ // 이름과 위치 정보
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Text(
+ text = name,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = location,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ // 수정 모드일 때 수정 버튼 표시
+ if (isEditMode) {
+ IconButton(
+ onClick = onEditClick,
+ colors = IconButtonDefaults.iconButtonColors(
+ containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
+ contentColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = "수정",
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ } else {
+ // 일반 모드일 때 상태 아이콘
+ Icon(
+ imageVector = statusIcon,
+ contentDescription = "상태",
+ modifier = Modifier.size(24.dp),
+ tint = statusColor
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 20e2a01..b6b3ddd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,4 +20,7 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
\ No newline at end of file
+android.nonTransitiveRClass=true
+
+# 한글 경로 문제 해결 (non-ASCII characters)
+android.overridePathCheck=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8d8ce30..592b479 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,7 @@
[versions]
-agp = "8.12.0"
+#agp = "8.12.0"
+agp = "8.11.1"
+
kotlin = "2.2.10"
coreKtx = "1.16.0"
junit = "4.13.2"
@@ -98,6 +100,7 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" } # 새로추가함
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }