diff --git a/frontend/app/build.gradle.kts b/frontend/app/build.gradle.kts index f0d6aa0f..f1b4305f 100644 --- a/frontend/app/build.gradle.kts +++ b/frontend/app/build.gradle.kts @@ -73,6 +73,12 @@ dependencies { implementation("androidx.test.ext:junit-ktx:1.1.5") implementation("androidx.compose.foundation:foundation-layout-android:1.5.4") implementation("io.coil-kt:coil-compose:2.5.0") + + implementation("androidx.work:work-runtime-ktx:2.8.0") + androidTestImplementation("androidx.work:work-testing:2.8.0") + + implementation("com.squareup.picasso:picasso:2.8") + testImplementation("junit:junit:4.13.2") testImplementation("org.mockito:mockito-core:4.11.0") testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") @@ -86,8 +92,8 @@ dependencies { androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.5.4") debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") - implementation("com.google.android.gms:play-services-location:17.0.0") - implementation("com.google.android.gms:play-services-maps:17.0.0") + implementation("com.google.android.gms:play-services-location:21.0.1") + implementation("com.google.android.gms:play-services-maps:18.2.0") implementation("com.google.maps.android:maps-compose:2.11.4") implementation("androidx.activity:activity-ktx:1.3.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") diff --git a/frontend/app/src/main/AndroidManifest.xml b/frontend/app/src/main/AndroidManifest.xml index 64a09753..63bf218f 100644 --- a/frontend/app/src/main/AndroidManifest.xml +++ b/frontend/app/src/main/AndroidManifest.xml @@ -36,7 +36,7 @@ android:theme="@style/Theme.Frontend" /> diff --git a/frontend/app/src/main/java/com/example/frontend/MapActivity.kt b/frontend/app/src/main/java/com/example/frontend/MainActivity.kt similarity index 74% rename from frontend/app/src/main/java/com/example/frontend/MapActivity.kt rename to frontend/app/src/main/java/com/example/frontend/MainActivity.kt index 83a611f2..81fbe33d 100644 --- a/frontend/app/src/main/java/com/example/frontend/MapActivity.kt +++ b/frontend/app/src/main/java/com/example/frontend/MainActivity.kt @@ -10,54 +10,46 @@ import android.os.Bundle import android.provider.Settings import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.frontend.ui.component.BottomBar -import com.example.frontend.ui.component.MapWithMarker +import androidx.lifecycle.lifecycleScope +import com.example.frontend.ui.map.FriendsMapUI import com.example.frontend.ui.theme.FrontendTheme -import com.example.frontend.viewmodel.FriendsViewModel +import com.example.frontend.usecase.CheckInUseCase +import com.example.frontend.usecase.PeriodicCheckInUseCase +import com.example.frontend.usecase.SaveMyInfoUseCase import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationCallback import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.LocationResult -import com.google.android.gms.location.LocationServices import com.google.android.gms.maps.model.LatLng import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint -class MapActivity : ComponentActivity() { - private lateinit var fusedLocationClient: FusedLocationProviderClient +class MainActivity : ComponentActivity() { + @Inject + lateinit var fusedLocationClient: FusedLocationProviderClient var currentLocation by mutableStateOf(null) + @Inject + lateinit var checkInUseCase: CheckInUseCase + + @Inject + lateinit var saveMyInfoUseCase: SaveMyInfoUseCase + private fun hasBackgroundLocationPermission(context: Context): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { ContextCompat.checkSelfPermission( @@ -108,7 +100,6 @@ class MapActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) setContent { FrontendTheme { @@ -127,11 +118,16 @@ class MapActivity : ComponentActivity() { } checkAndRequestLocationPermissions() + + PeriodicCheckInUseCase(this).execute() + + lifecycleScope.launch { + saveMyInfoUseCase.execute() + } } override fun onResume() { super.onResume() - fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) setContent { FrontendTheme { @@ -222,45 +218,3 @@ class MapActivity : ComponentActivity() { } } -@Composable -fun FriendsMapUI(currentLocation: LatLng?, onClick: () -> Unit) { - val viewModel: FriendsViewModel = viewModel() - val friendsList by viewModel.friendsList.observeAsState(emptyList()) - val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(Unit) { - while (isActive) { - viewModel.fetchFriends() - delay(3000) - } - } - - Scaffold( - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - }, - bottomBar = { - BottomBar(currentLocation) - } - ) { paddingValues -> - Box( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize(), - contentAlignment = Alignment.BottomEnd - ) { - MapWithMarker(currentLocation, friendsList) - FloatingActionButton( - onClick = { onClick() }, - modifier = Modifier.padding(16.dp) - ) { - Icon(Icons.Filled.Add, contentDescription = null) - } - - } - - } - - -} - - diff --git a/frontend/app/src/main/java/com/example/frontend/MeetupListUI.kt b/frontend/app/src/main/java/com/example/frontend/MeetupListUI.kt index 79edbd6a..db78b1b8 100644 --- a/frontend/app/src/main/java/com/example/frontend/MeetupListUI.kt +++ b/frontend/app/src/main/java/com/example/frontend/MeetupListUI.kt @@ -159,7 +159,7 @@ fun ShowMeetUpUIPreview() { modifier = Modifier .size(46.dp) .clickable { - val nextIntent = Intent(context, MapActivity::class.java) + val nextIntent = Intent(context, MainActivity::class.java) context.startActivity(nextIntent) // finish current activity if (context is Activity) { diff --git a/frontend/app/src/main/java/com/example/frontend/SplashScreenActivity.kt b/frontend/app/src/main/java/com/example/frontend/SplashScreenActivity.kt index 8e1266b7..dfaba1b5 100644 --- a/frontend/app/src/main/java/com/example/frontend/SplashScreenActivity.kt +++ b/frontend/app/src/main/java/com/example/frontend/SplashScreenActivity.kt @@ -17,7 +17,7 @@ class SplashScreenActivity : AppCompatActivity() { if (userContextRepository.getIsLoggedIn()) { // User is already logged in, skip the login activity - startActivity(Intent(this, MapActivity::class.java)) + startActivity(Intent(this, MainActivity::class.java)) } else { // User is not logged in, go to the login activity startActivity(Intent(this, LoginActivity::class.java)) diff --git a/frontend/app/src/main/java/com/example/frontend/api/CheckInService.kt b/frontend/app/src/main/java/com/example/frontend/api/CheckInService.kt new file mode 100644 index 00000000..d7f01a5f --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/api/CheckInService.kt @@ -0,0 +1,41 @@ +package com.example.frontend.api + +import android.content.Context +import com.example.frontend.interceptor.AuthInterceptor +import com.example.frontend.model.CheckInModel +import com.example.frontend.utilities.BASE_URL +import com.example.frontend.utilities.GsonProvider +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.Body +import retrofit2.http.POST + +/** + * Used to connect to the server + * About check in domain + */ +interface CheckInService { + @POST("/check_ins") + fun login(@Body checkInModel: CheckInModel): Call? + + companion object { + fun create(context: Context): CheckInService { + val logger = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC } + + val client = OkHttpClient.Builder() + .addInterceptor(logger) + .addInterceptor(AuthInterceptor(context)) + .build() + + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create(GsonProvider.gson)) + .build() + .create(CheckInService::class.java) + } + } +} diff --git a/frontend/app/src/main/java/com/example/frontend/api/UserService.kt b/frontend/app/src/main/java/com/example/frontend/api/UserService.kt index 73935121..83c03373 100644 --- a/frontend/app/src/main/java/com/example/frontend/api/UserService.kt +++ b/frontend/app/src/main/java/com/example/frontend/api/UserService.kt @@ -11,6 +11,9 @@ interface UserService { @GET("/users") suspend fun getAllUsers(): List + @GET("/users/me") + suspend fun getMyInfo(): UserModel + @GET("/friends") suspend fun getAllFriends(): List @@ -22,4 +25,4 @@ interface UserService { @POST("/friends/{id}/confirm") suspend fun confirmRequest(@Path("id") friendId: Long) -} \ No newline at end of file +} diff --git a/frontend/app/src/main/java/com/example/frontend/di/AppModule.kt b/frontend/app/src/main/java/com/example/frontend/di/AppModule.kt new file mode 100644 index 00000000..0117efcb --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/di/AppModule.kt @@ -0,0 +1,52 @@ +package com.example.frontend.di + +import android.content.Context +import com.example.frontend.api.FriendService +import com.example.frontend.api.UserService +import com.example.frontend.interceptor.AuthInterceptor +import com.example.frontend.repository.FriendsRepository +import com.example.frontend.utilities.BASE_URL +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + fun provideRetrofit(@ApplicationContext context: Context): Retrofit { + val authInterceptor = AuthInterceptor(context) + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(authInterceptor) + .build() + + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + @Provides + fun provideFriendsRepository(api: FriendService): FriendsRepository { + return FriendsRepository(api) + } + + @Provides + fun provideFriendAPI(retrofit: Retrofit): FriendService { + return retrofit.create(FriendService::class.java) + } + + @Provides + fun provideUserService(retrofit: Retrofit): UserService { + return retrofit.create(UserService::class.java) + } + +} diff --git a/frontend/app/src/main/java/com/example/frontend/di/LocationModule.kt b/frontend/app/src/main/java/com/example/frontend/di/LocationModule.kt new file mode 100644 index 00000000..cd242dc7 --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/di/LocationModule.kt @@ -0,0 +1,26 @@ +package com.example.frontend.di + +import android.content.Context +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + + +/* + * 지리정보 관련 데이터를 제공하는 모듈 + */ +@Module +@InstallIn(SingletonComponent::class) +object LocationModule { + + @Provides + @Singleton + fun provideFusedLocationProviderClient(@ApplicationContext context: Context): FusedLocationProviderClient { + return LocationServices.getFusedLocationProviderClient(context) + } +} diff --git a/frontend/app/src/main/java/com/example/frontend/model/CheckInModel.kt b/frontend/app/src/main/java/com/example/frontend/model/CheckInModel.kt new file mode 100644 index 00000000..c1d6bb59 --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/model/CheckInModel.kt @@ -0,0 +1,6 @@ +package com.example.frontend.model + +data class CheckInModel( + val latitude: Double, + val longitude: Double, +) diff --git a/frontend/app/src/main/java/com/example/frontend/repository/FriendsRepository.kt b/frontend/app/src/main/java/com/example/frontend/repository/FriendsRepository.kt index 4a316374..78614bc2 100644 --- a/frontend/app/src/main/java/com/example/frontend/repository/FriendsRepository.kt +++ b/frontend/app/src/main/java/com/example/frontend/repository/FriendsRepository.kt @@ -1,23 +1,11 @@ package com.example.frontend.repository -import android.content.Context import com.example.frontend.api.FriendService -import com.example.frontend.api.UserService import com.example.frontend.callback.FriendLocationCallback -import com.example.frontend.interceptor.AuthInterceptor import com.example.frontend.model.UserWithLocationModel -import com.example.frontend.utilities.BASE_URL -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import okhttp3.OkHttpClient import retrofit2.Call import retrofit2.Callback import retrofit2.Response -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory class FriendsRepository(private val friendService: FriendService) { @@ -43,39 +31,3 @@ class FriendsRepository(private val friendService: FriendService) { }) } } - -@Module -@InstallIn(SingletonComponent::class) -object AppModule { - - @Provides - fun provideRetrofit(@ApplicationContext context: Context): Retrofit { - val authInterceptor = AuthInterceptor(context) - - val okHttpClient = OkHttpClient.Builder() - .addInterceptor(authInterceptor) - .build() - - return Retrofit.Builder() - .baseUrl(BASE_URL) - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) - .build() - } - - @Provides - fun provideFriendsRepository(api: FriendService): FriendsRepository { - return FriendsRepository(api) - } - - @Provides - fun provideFriendAPI(retrofit: Retrofit): FriendService { - return retrofit.create(FriendService::class.java) - } - - @Provides - fun provideUserService(retrofit: Retrofit): UserService { - return retrofit.create(UserService::class.java) - } - -} diff --git a/frontend/app/src/main/java/com/example/frontend/repository/UserContextRepository.kt b/frontend/app/src/main/java/com/example/frontend/repository/UserContextRepository.kt index 194c83b4..7eeb6964 100644 --- a/frontend/app/src/main/java/com/example/frontend/repository/UserContextRepository.kt +++ b/frontend/app/src/main/java/com/example/frontend/repository/UserContextRepository.kt @@ -52,6 +52,14 @@ class UserContextRepository( return store.getBoolean(IS_LOGGED_IN) ?: false } + fun getUserId(): Int? { + return store.getInt(USER_ID) + } + + fun saveUserId(userId: Int) { + store.putInt(USER_ID, userId) + } + fun clear() { store.clear() } @@ -75,5 +83,6 @@ class UserContextRepository( private const val AUTH_TOKEN = "AUTH_TOKEN" private const val SELECTED_PREDEFINED_IMAGE = "SELECTED_PREDEFINED_IMAGE" private const val IS_LOGGED_IN = "IS_LOGGED_IN" + private const val USER_ID = "USER_ID" } } diff --git a/frontend/app/src/main/java/com/example/frontend/repository/UsersRepository.kt b/frontend/app/src/main/java/com/example/frontend/repository/UsersRepository.kt index 6c357a04..8cc27440 100644 --- a/frontend/app/src/main/java/com/example/frontend/repository/UsersRepository.kt +++ b/frontend/app/src/main/java/com/example/frontend/repository/UsersRepository.kt @@ -10,6 +10,10 @@ class UsersRepository @Inject constructor(private val userService: UserService) return userService.getAllUsers() } + suspend fun getMyInfo(): UserModel { + return userService.getMyInfo() + } + suspend fun getFriends(): List { return userService.getAllFriends() } diff --git a/frontend/app/src/main/java/com/example/frontend/ui/component/BottomBar.kt b/frontend/app/src/main/java/com/example/frontend/ui/component/BottomBar.kt index 36f1e4e6..41c9c78d 100644 --- a/frontend/app/src/main/java/com/example/frontend/ui/component/BottomBar.kt +++ b/frontend/app/src/main/java/com/example/frontend/ui/component/BottomBar.kt @@ -22,15 +22,14 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.example.frontend.MeetupActivity +import com.example.frontend.MainActivity import com.example.frontend.MeetupListUI import com.example.frontend.MissionActivity import com.example.frontend.ui.friend.FriendActivity import com.example.frontend.ui.settings.UserInfoActivity -import com.google.android.gms.maps.model.LatLng @Composable -fun BottomBar(currentLocation: LatLng?) { +fun BottomBar() { val context = LocalContext.current val icons = listOf( Icons.Default.Star, @@ -57,10 +56,14 @@ fun BottomBar(currentLocation: LatLng?) { IconToggleButton(icon = icon) { when (icon) { icons[0] -> { - // MeetUp 생성으로 이동 - val nextIntent = Intent(context, MeetupActivity::class.java) - nextIntent.putExtra("currentLocation", currentLocation) - context.startActivity(nextIntent) + // If current activity is MainActivity, do nothing + if (context.javaClass.simpleName == "MainActivity") { + return@IconToggleButton + } else { + // MainActivity 이동 + val nextIntent = Intent(context, MainActivity::class.java) + context.startActivity(nextIntent) + } } icons[1] -> { @@ -96,5 +99,5 @@ fun BottomBar(currentLocation: LatLng?) { @Composable @Preview private fun BottomBarPreview() { - BottomBar(null) + BottomBar() } diff --git a/frontend/app/src/main/java/com/example/frontend/ui/component/MapUI.kt b/frontend/app/src/main/java/com/example/frontend/ui/component/MapUI.kt index bbe55060..c758592d 100644 --- a/frontend/app/src/main/java/com/example/frontend/ui/component/MapUI.kt +++ b/frontend/app/src/main/java/com/example/frontend/ui/component/MapUI.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import com.example.frontend.model.UserWithLocationModel import com.example.frontend.ui.theme.FrontendTheme @@ -12,15 +13,18 @@ import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapUiSettings import com.google.maps.android.compose.Marker import com.google.maps.android.compose.MarkerState +import com.google.maps.android.compose.Polygon import com.google.maps.android.compose.rememberCameraPositionState import kotlin.random.Random @Composable fun MapWithMarker( currentLocation: LatLng?, - friends: List + friends: List, + polygon: List? = null ) { Box( modifier = Modifier @@ -33,7 +37,8 @@ fun MapWithMarker( GoogleMap( modifier = Modifier.fillMaxSize(), - cameraPositionState = cameraPositionState + cameraPositionState = cameraPositionState, + uiSettings = MapUiSettings(zoomControlsEnabled = false) ) { Marker( state = MarkerState(position = location), @@ -41,6 +46,13 @@ fun MapWithMarker( snippet = "You are here" ) + polygon?.let { locations -> + Polygon( + points = locations, + fillColor = Color(0x89CFF0FF) + ) + } + friends.forEach { buildMarkerIcon(it) } } } diff --git a/frontend/app/src/main/java/com/example/frontend/ui/map/FriendsMapUI.kt b/frontend/app/src/main/java/com/example/frontend/ui/map/FriendsMapUI.kt new file mode 100644 index 00000000..d5dcc140 --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/ui/map/FriendsMapUI.kt @@ -0,0 +1,65 @@ +package com.example.frontend.ui.map + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.frontend.ui.component.BottomBar +import com.example.frontend.ui.component.MapWithMarker +import com.example.frontend.utilities.SNU_POLYGON +import com.example.frontend.viewmodel.FriendsViewModel +import com.google.android.gms.maps.model.LatLng +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive + +@Composable +fun FriendsMapUI(currentLocation: LatLng?, onClick: () -> Unit) { + val viewModel: FriendsViewModel = viewModel() + val friendsList by viewModel.friendsList.observeAsState(emptyList()) + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(Unit) { + while (isActive) { + viewModel.fetchFriends() + delay(3000) + } + } + + Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + bottomBar = { + BottomBar() + } + ) { paddingValues -> + Box( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + contentAlignment = Alignment.BottomEnd + ) { + MapWithMarker(currentLocation, friendsList, SNU_POLYGON) + FloatingActionButton( + onClick = { onClick() }, + modifier = Modifier.padding(16.dp) + ) { + Icon(Icons.Filled.Add, contentDescription = null) + } + } + } +} diff --git a/frontend/app/src/main/java/com/example/frontend/ui/map/ProfileDialog.kt b/frontend/app/src/main/java/com/example/frontend/ui/map/ProfileDialog.kt new file mode 100644 index 00000000..a5611244 --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/ui/map/ProfileDialog.kt @@ -0,0 +1,87 @@ +package com.example.frontend.ui.map + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.example.frontend.data.predefinedImages + +/* + * 유저의 프로필을 보여주는 Dialog + */ +@Composable +internal fun ProfileDialog( + title: String, + description: String, + onDismiss: () -> Unit = {}, + painter: Painter, +) { + return AlertDialog( + properties = DialogProperties(usePlatformDefaultWidth = false), + onDismissRequest = { onDismiss() }, + title = { + Row( + modifier = Modifier.padding(bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) + { + Image( + painter = painter, + contentDescription = "Profile Image", + modifier = Modifier + .size(80.dp) + .clip(RoundedCornerShape(40.dp)) + ) + Text( + text = title, + style = MaterialTheme.typography.headlineLarge, + modifier = Modifier.padding(start = 16.dp), + ) + } + }, + text = { + HorizontalDivider() + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(vertical = 16.dp), + ) + }, + confirmButton = { + Text( + text = "닫기", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(horizontal = 8.dp) + .clickable { onDismiss() }, + ) + }, + ) +} + + +@Composable +@Preview +private fun ProfileDialogPreview() { + ProfileDialog( + title = "호에엥맨", + description = "큭큭", + painter = painterResource(id = predefinedImages[0]), + ) +} diff --git a/frontend/app/src/main/java/com/example/frontend/usecase/CheckInUseCase.kt b/frontend/app/src/main/java/com/example/frontend/usecase/CheckInUseCase.kt new file mode 100644 index 00000000..f6e8df1f --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/usecase/CheckInUseCase.kt @@ -0,0 +1,38 @@ +package com.example.frontend.usecase + +import android.content.Context +import android.util.Log +import com.example.frontend.api.CheckInService +import com.example.frontend.model.CheckInModel +import dagger.hilt.android.qualifiers.ApplicationContext +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import javax.inject.Inject + +/* + * 체크인을 수행하는 메서드 + */ +class CheckInUseCase @Inject constructor( + @ApplicationContext context: Context +) { + private val checkInService = CheckInService.create(context) + + fun execute(latitude: Double, longitude: Double) { + val checkInModel = CheckInModel(latitude, longitude) + checkInService.login(checkInModel)?.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + val body = response.body() + Log.d("CheckInUseCase", "Check in successful: $body") + } else { + Log.d("CheckInUseCase", "Check in failed: ${response.message()}") + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("CheckInUseCase", "Check in failed: ${t.message}") + } + }) + } +} diff --git a/frontend/app/src/main/java/com/example/frontend/usecase/PeriodicCheckInUseCase.kt b/frontend/app/src/main/java/com/example/frontend/usecase/PeriodicCheckInUseCase.kt new file mode 100644 index 00000000..596f8625 --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/usecase/PeriodicCheckInUseCase.kt @@ -0,0 +1,46 @@ +package com.example.frontend.usecase + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import androidx.core.app.ActivityCompat +import com.example.frontend.utilities.PeriodicTask +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority + +class PeriodicCheckInUseCase(private val context: Context) { + private val checkInUseCase = CheckInUseCase(context) + private val locationClient = LocationServices.getFusedLocationProviderClient(context) + + fun execute() { + + PeriodicTask( + command = { recordLocation() }, + intervalInMinutes = INTERVAL_IN_MINUTES + ).execute() + } + + private fun recordLocation() { + Log.i(TAG, "execute recordLocation") + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED + ) { + Log.e(TAG, "No location permission") + return + } + + locationClient.getCurrentLocation(PRIORITY, null).addOnSuccessListener { location -> + location?.let { + Log.i(TAG, "Location: $location") + checkInUseCase.execute(location.latitude, location.longitude) + } + } + } + + companion object { + private const val TAG = "PeriodicCheckInUseCase" + private const val PRIORITY = Priority.PRIORITY_HIGH_ACCURACY + private const val INTERVAL_IN_MINUTES = 5L + } +} diff --git a/frontend/app/src/main/java/com/example/frontend/usecase/SaveMyInfoUseCase.kt b/frontend/app/src/main/java/com/example/frontend/usecase/SaveMyInfoUseCase.kt new file mode 100644 index 00000000..7757caed --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/usecase/SaveMyInfoUseCase.kt @@ -0,0 +1,28 @@ +package com.example.frontend.usecase + +import android.content.Context +import android.util.Log +import com.example.frontend.repository.UserContextRepository +import com.example.frontend.repository.UsersRepository +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +/* + * users/me API를 바탕으로 사용자 정보를 저장하는 UseCase + */ +class SaveMyInfoUseCase @Inject constructor( + private val repository: UsersRepository, + @ApplicationContext context: Context +) { + private val userContextRepository = UserContextRepository.ofContext(context) + + suspend fun execute() { + Log.i("SaveMyInfoUseCase", "execute") + + repository.getMyInfo().apply { + userContextRepository.saveUserName(name) + userContextRepository.saveUserMail(email) + userContextRepository.saveUserId(id.toInt()) + } + } +} diff --git a/frontend/app/src/main/java/com/example/frontend/usecase/login/LoginUseCase.kt b/frontend/app/src/main/java/com/example/frontend/usecase/login/LoginUseCase.kt index a1623e8c..444ba84d 100644 --- a/frontend/app/src/main/java/com/example/frontend/usecase/login/LoginUseCase.kt +++ b/frontend/app/src/main/java/com/example/frontend/usecase/login/LoginUseCase.kt @@ -5,7 +5,7 @@ import android.content.Context import android.content.Intent import android.widget.Toast import androidx.compose.runtime.MutableState -import com.example.frontend.MapActivity +import com.example.frontend.MainActivity import com.example.frontend.api.AuthService import com.example.frontend.model.AuthResponse import com.example.frontend.model.LoginModel @@ -29,7 +29,7 @@ class LoginUseCase( val loginModel = LoginModel(email, password) if (BYPASS_LOGIN) { // TODO(heka1024): Remove this flag - val nextIntent = Intent(context, MapActivity::class.java) + val nextIntent = Intent(context, MainActivity::class.java) context.startActivity(nextIntent) if (context is Activity) { context.finish() @@ -44,7 +44,7 @@ class LoginUseCase( saveAuthToken(authToken) result.value = "Logged in successfully" - val nextIntent = Intent(context, MapActivity::class.java) + val nextIntent = Intent(context, MainActivity::class.java) context.startActivity(nextIntent) if (context is Activity) { diff --git a/frontend/app/src/main/java/com/example/frontend/utilities/Location.kt b/frontend/app/src/main/java/com/example/frontend/utilities/Location.kt new file mode 100644 index 00000000..3856a635 --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/utilities/Location.kt @@ -0,0 +1,24 @@ +package com.example.frontend.utilities + +import com.google.android.gms.maps.model.LatLng + +/* + * 서울대 폴리곤모양을 적어놓은 것 + */ +val SNU_POLYGON = listOf( + LatLng(37.469020, 126.952321), + LatLng(37.467113, 126.947597), + LatLng(37.461482, 126.948941), + LatLng(37.459540, 126.947481), + LatLng(37.455836, 126.948220), + LatLng(37.450913, 126.949656), + LatLng(37.447540, 126.949270), + LatLng(37.447097, 126.951417), + LatLng(37.453400, 126.953478), + LatLng(37.457326, 126.956584), + LatLng(37.462478, 126.959963), + LatLng(37.467095, 126.960976), + LatLng(37.468567, 126.957310), +) + +val BUILDING_302 = LatLng(37.466967, 126.950302) diff --git a/frontend/app/src/main/java/com/example/frontend/utilities/PeriodicTask.kt b/frontend/app/src/main/java/com/example/frontend/utilities/PeriodicTask.kt new file mode 100644 index 00000000..669a18d2 --- /dev/null +++ b/frontend/app/src/main/java/com/example/frontend/utilities/PeriodicTask.kt @@ -0,0 +1,28 @@ +package com.example.frontend.utilities + +import android.os.Handler +import android.os.Looper +import android.util.Log +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +/* + * Run a periodic task + */ +class PeriodicTask( + private val command: Runnable, + private val intervalInMinutes: Long = 5, +) { + + fun execute() { + Log.i("PeriodicTask", "execute") + + val service = Executors.newSingleThreadScheduledExecutor() + val handler = Handler(Looper.getMainLooper()) + service.scheduleAtFixedRate( + handler.run { command }, + 0, intervalInMinutes, TimeUnit.MINUTES + ) + } +} + diff --git a/frontend/app/src/main/res/drawable/map_pin_svgrepo_com.xml b/frontend/app/src/main/res/drawable/map_pin_svgrepo_com.xml new file mode 100644 index 00000000..90f1f34d --- /dev/null +++ b/frontend/app/src/main/res/drawable/map_pin_svgrepo_com.xml @@ -0,0 +1,9 @@ + + + diff --git a/frontend/app/src/main/res/drawable/white_circle.xml b/frontend/app/src/main/res/drawable/white_circle.xml new file mode 100644 index 00000000..522eb166 --- /dev/null +++ b/frontend/app/src/main/res/drawable/white_circle.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/frontend/settings.gradle.kts b/frontend/settings.gradle.kts index 08a0d6ae..9cee02fb 100644 --- a/frontend/settings.gradle.kts +++ b/frontend/settings.gradle.kts @@ -15,4 +15,3 @@ dependencyResolutionManagement { rootProject.name = "frontend" include(":app") - \ No newline at end of file