[안드로이드] Wifi기반 체류 시간 앱 - 3

우선 이전에 정했던 erd에서 수정 사항이 생겼다

 

 

다음과 같이 user를 처음 등록할때 wifi의 정보를 얻어 저장하고, WifiLog에는 log만 기록하려 한다

 

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    user_name = Column(String(50), unique=True, index=True, nullable=False)
    home_ssid = Column(String(50), nullable=False)
    home_bssid = Column(String(50), nullable=False)
    logs = relationship("WifiLog", back_populates="user")

 

class WifiLog(Base):
    __tablename__ = "wifi_logs"

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    start_time = Column(DateTime, server_default=func.now(), nullable=False)
    end_time = Column(DateTime, nullable=True)

    user = relationship("User", back_populates="logs")

 

그렇기에 다음과 같이 model을 바꿨으며

 

class UserBase(BaseModel):
    user_name: str
    home_ssid: str
    home_bssid: str

 

데이터 입출력을 위한 Schema도 변경하였다

 

이후 Kotlin으로 넘어가게 되면 우선 Wifi의 정보를 얻기위해 permission을 열어야 한다

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <application
        android:usesCleartextTraffic="true"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.App">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.App">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>

 

위 처럼 wifi 정보를 얻기 위해 정확한 location 정보가 필요하다 이후 Network의 정보를 받아오는 식이다

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

 

그리고 다음과 같이 구성하였다

 

https://joo-selfdev.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-MVVM-%EC%A0%95%EB%A6%AC#google_vignette

 

안드로이드 디자인 패턴: MVVM 완벽 정리

안녕하세요!이번 글에서는 MVVM(Model-View-ViewModel) 패턴에 대해 자세히 알아보겠습니다.MVVM 패턴은 MVC, MVP 패턴의 단점을 보완하고 UI와 비즈니스 로직을 분리하는데 중점을 둡니다.이제 MVVM 패턴의

joo-selfdev.tistory.com

 

다음의 형식처럼 MVVM 디자인 패턴을 채용하였다

 

package com.example.app.data

data class UserCreateRequest(
    val user_name : String,
    val home_ssid: String,
    val home_bssid: String
)

 

다음과 같이 dataclass를 지정하였으며

 

package com.example.app.network
import com.example.app.data.UserCreateRequest
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface ApiService {
    @POST("user/create")
    suspend fun registerUser(@Body request: UserCreateRequest): Response<Unit>
}

 

다음의 ApiService Interface와

 

package com.example.app.network
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitClient {
    private const val BASE_URL = "http://192.168.0.37:8000/"
    //test용 URL
    val instance: ApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

 

Client를 구성하였고

 

// 화면에 표시될 모든 정보를 담는 데이터 클래스
data class WifiUiState(
    val ssid: String = "",
    val bssid: String = "",
    val message: String = "Wi-Fi 정보를 가져오는 중...",
    val registrationStatus: String = ""
)

class ViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(WifiUiState())
    val uiState: StateFlow<WifiUiState> = _uiState.asStateFlow()

    // wifi 정보 가져오는 기능
    fun getWifiInfo(context: Context) {
        // 위치 권한이 없으면 실행 중단
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            _uiState.update { it.copy(message = "Wi-Fi 정보를 보려면 위치 권한이 필요합니다.") }
            return
        }
        val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val network = connectivityManager.activeNetwork
        val capabilities = connectivityManager.getNetworkCapabilities(network)

        if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
            // WifiManager.connectionInfo is deprecated but seems more reliable than transportInfo in some cases.
            // It should work on newer Android versions if ACCESS_FINE_LOCATION is granted.
            @Suppress("DEPRECATION")
            val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as android.net.wifi.WifiManager
            @Suppress("DEPRECATION")
            val wifiInfo = wifiManager.connectionInfo

            val ssid = wifiInfo?.ssid?.removeSurrounding("\"") ?: ""
            val bssid = wifiInfo?.bssid ?: ""

            if (ssid.isNotEmpty() && ssid != "<unknown ssid>") {
                _uiState.update { it.copy(ssid = ssid, bssid = bssid, message = "") }
            } else {
                _uiState.update { it.copy(message = "SSID를 찾을 수 없습니다. 위치 서비스가 켜져 있는지 확인하세요.") }
            }
        } else {
            _uiState.update { it.copy(message = "Wi-Fi에 연결되어 있지 않습니다.") }
        }
    }

    //wifi 정보 등록하는 기능
    fun registerWifiInfo(userName: String) {
        val currentState = _uiState.value
        if (userName.isBlank() || currentState.ssid.isBlank()) {
            _uiState.update { it.copy(registrationStatus = "이름과 Wi-Fi 정보가 모두 필요합니다.") }
            return
        }

        viewModelScope.launch {
            try {
                _uiState.update { it.copy(registrationStatus = "등록 중...") }
                val request = UserCreateRequest(userName, currentState.ssid, currentState.bssid)
                val response = RetrofitClient.instance.registerUser(request)
                if (response.isSuccessful) {
                    _uiState.update { it.copy(registrationStatus = "등록 성공!") }
                } else {
                    _uiState.update { it.copy(registrationStatus = "등록 실패 (코드: ${response.code()})") }
                }
            } catch (e: Exception) {
                _uiState.update { it.copy(registrationStatus = "오류 발생: ${e.message}") }
            }
        }
    }
}

 

ViewModel을 다음과 같이 지정하였다

 

class MainActivity : ComponentActivity() {

    private val viewModel: ViewModel by viewModels()

    // 권한 요청 결과를 처리하는 부분
    private val requestPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
            if (isGranted) {
                // 권한을 허용하면 Wi-Fi 정보 가져오기 시도
                viewModel.getWifiInfo(this)
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 앱이 켜지자마자 위치 권한 요청
        requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)

        setContent {
            AppTheme {
                // ViewModel의 상태가 바뀔 때마다 화면이 자동으로 업데이트됨
                val uiState by viewModel.uiState.collectAsState()

                WifiInfoScreen(
                    uiState = uiState,
                    onRefresh = { viewModel.getWifiInfo(this) },
                    onRegister = { userName -> viewModel.registerWifiInfo(userName) }
                )
            }
        }
    }
}

// 화면 UI를 그리는 부분
@Composable
fun WifiInfoScreen(
    uiState: WifiUiState,
    onRefresh: () -> Unit,
    onRegister: (String) -> Unit
) {
    var userName by remember { mutableStateOf("") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        if (uiState.message.isNotEmpty()) {
            Text(text = uiState.message, color = MaterialTheme.colorScheme.error)
        } else {
            Text(text = "SSID: ${uiState.ssid}")
            Spacer(modifier = Modifier.height(8.dp))
            Text(text = "BSSID: ${uiState.bssid}")
        }

        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = onRefresh) {
            Text("새로고침")
        }

        Spacer(modifier = Modifier.height(32.dp))
        OutlinedTextField(
            value = userName,
            onValueChange = { userName = it },
            label = { Text("사용자 이름") },
            singleLine = true
        )

        Spacer(modifier = Modifier.height(16.dp))
        Button(
            onClick = { onRegister(userName) },
            enabled = uiState.ssid.isNotEmpty() && userName.isNotEmpty()
        ) {
            Text("서버에 등록")
        }

        if (uiState.registrationStatus.isNotEmpty()) {
            Spacer(modifier = Modifier.height(16.dp))
            Text(text = uiState.registrationStatus)
        }
    }
}

 

이후 MainActivity는 다음과 같이 하였다

하지만 중간에 다음의 문제가 발생하였다

 

 

이 문제는 http일 경우 생기는 문제로 다음의 블로그를 보고 다음의 방식으로 해결하였다

다음의 방식은 우선적으로 http가 가능하게 임시로 열어두는 것이며 추후에 https까지 생각을 해야할 것 같다

android:usesCleartextTraffic="true"

https://gun0912.tistory.com/80

 

[안드로이드]CLEARTEXT communication to XXXX not permitted by network security policy

"CLEARTEXT communication to XXXX not permitted by network security policy" 어느날 코드를 바꾼게 없는데도 위와 같은 오류가 발생하면서 앱이 실행이 안되는 일이 발생합니다.그 이유는 여러분 혹은 사용자폰의 O

gun0912.tistory.com

 

이후 등록되는 화면은 다음과 같다

 

 

그리고 DB에도 잘 들어가는 모습이다

 

 

이후에 작업은 다음과 같다

 

1. 체류 시간 로컬 로깅

2. API 및 DB 연동

3. 통계 분석 API 개발

4. 통계 시각화

5. 클라우드 배포

 

다음 일차는 체류 시간 로컬 로깅을 해봐야겠다