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

체류 시간 로컬 로깅에 대해 진행하겠다

 

우선 backend 부터 시작했다

다음의 log_api를 만들었으며

from sqlalchemy.orm import Session
import models, schemas
from datetime import date, datetime, timezone, timedelta


# 한국 시간대 설정
KST = timezone(timedelta(hours=9))

# 새로운 로그 생성
def start_log(db: Session, wifi_log: schemas.WifiLogCreate):
    db_wifi_log = models.WifiLog(
        user_id=wifi_log.user_id,
        start_time=datetime.now(KST)
    )
    db.add(db_wifi_log)
    db.commit()    
    db.refresh(db_wifi_log)
    return db_wifi_log

# 전체 사용자 목록
def get_all_logs(db: Session, start: int = 0, last: int = 10):
    return db.query(models.WifiLog).offset(start).limit(last).all()

# 사용자 id로 사용자 반환
def get_log_by_id(db: Session, user_id: int):
    return db.query(models.WifiLog).filter(models.WifiLog.user_id == user_id).first()

def end_log(db: Session, log_id: int):
    db_log = db.query(models.WifiLog).filter(models.WifiLog.id == log_id).first()
    if db_log is None:
        return None
    
    db_log.end_time = datetime.now(KST)
    db.commit()
    db.refresh(db_log)
    return db_log

 

다음의 log api router이다

from fastapi import APIRouter
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List

from database import get_db
import schemas
from api import log_api

wifi_log = APIRouter(prefix="/log", tags=["log"])


@wifi_log.post("/start", response_model=schemas.WifiLog)
def start_log(wifi_log: schemas.WifiLogCreate, db: Session = Depends(get_db)):
    return log_api.start_log(db=db, wifi_log=wifi_log)

@wifi_log.get("/id/{user_id}", response_model=schemas.WifiLog)
def read_log_by_id(user_id: int, db: Session = Depends(get_db)):
    db_log = log_api.get_log_by_id(db, user_id=user_id)
    if db_log is None:
        raise HTTPException(status_code=404, detail="Log not found")
    return db_log


@wifi_log.get("/list", response_model=List[schemas.WifiLog])
def read_logs(start: int = 0, last: int = 10, db: Session = Depends(get_db)):
    logs = log_api.get_all_logs(db, start=start, last=last)
    return logs

@wifi_log.post("/end", response_model=schemas.WifiLog)
def end_log(log_id: int, db: Session = Depends(get_db)):
    db_log = log_api.end_log(db=db, log_id=log_id)
    if db_log is None:
        raise HTTPException(status_code=404, detail="Log not found")
    return db_log

 

그리고 스키마도 다음과 같이 수정하였다

from datetime import datetime
from pydantic import BaseModel

# User 관련 스키마
class UserBase(BaseModel):
    user_name: str
    home_ssid: str
    home_bssid: str

class UserCreate(UserBase):
    pass

class User(UserBase):
    id: int

    class Config:
        from_attributes = True

# WifiLog 관련 스키마
class WifiLogBase(BaseModel):
    user_id: int

class WifiLogCreate(WifiLogBase):
    pass

class WifiLog(WifiLogBase):
    id: int
    start_time: datetime
    end_time: datetime | None = None
    
    class Config:
        from_attributes = True

 

그 후 안드로이드에서 작업을 이어 나갔다

 

1. wifi가 등록이 되었을때 등록되어 있는 wifi와 현재 wifi가 일치할 경우 로깅 시작

2. wifi에 연결을 하였는데 해당 wifi가 등록이 되어 있는 경우 로깅 시작

3. wifi가 해제 되었을 시 로깅 종료

4. 등록된 wifi에 연결을 하다가 다른 wifi에 연결을 하였는데 등록이 되어 있지않은 경우 로깅 종료

 

이런식으로 조건을 만들 수 있을 것이다

 

그리고 현재의 backend 로직 상 wifi 등록 시에 해당 user_id가 response되어 나오고, log할때 이가 필요하므로 우선 data class를 만들었다

 

package com.example.app.data

data class UserResponse(
    val user_name: String,
    val home_ssid: String,
    val home_bssid: String,
    val id: Int  // user_id
)

 

package com.example.app.data

data class StartLogRequest(
    val user_id: Int
)

 

그 이후 api service 인터페이스도 추가하였다

 

interface ApiService {
    @POST("user/create")
    suspend fun registerUser(@Body request: UserCreateRequest): UserResponse

    @POST("log/start")
    suspend fun startLog(@Body request: StartLogRequest): LogResponse

    @POST("log/end")
    suspend fun endLog(@Query("log_id") logId: Int): LogResponse
}

 

그리고 view model에서는

 

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 res: UserResponse = RetrofitClient.instance.registerUser(request)
            userId = res.id
            homeSsid = res.home_ssid
            homeBssid = res.home_bssid
            // TODO: DataStore에 userId 저장
            _uiState.update { it.copy(registrationStatus = "등록 성공 (user_id=${res.id})") }

            // 등록 직후 현재 Wi‑Fi가 집과 일치하면 즉시 로그 시작
            val now = _uiState.value
            if (currentLogId == null && isHomeWifi(now.ssid, now.bssid)) {
                startHomeLog()
            }
        } catch (e: Exception) {
            _uiState.update { it.copy(registrationStatus = "오류 발생: ${e.message}") }
        }
    }
}

 

위 1번 사항을 위한 기능과

 

//wifi 연결 시 호출
fun startHomeLog() {
    val uid = userId ?: return
    if (currentLogId != null) return // 이미 시작됨
    viewModelScope.launch {
        try {
            val log: LogResponse = RetrofitClient.instance.startLog(StartLogRequest(uid))
            currentLogId = log.id
            // TODO: DataStore에 currentLogId 저장
        } catch (_: Exception) { /* no-op */ }
    }
}

 

이후 현재 wifi와 등록된 wifi가 같다면 Log를 시작하며

 

//wifi 해제 시 호출
fun endHomeLog() {
    val logId = currentLogId ?: return
    viewModelScope.launch {
        try {
            RetrofitClient.instance.endLog(logId)
            currentLogId = null
            // TODO: DataStore에서 currentLogId 제거
        } catch (_: Exception) { /* no-op */ }
    }
}

// 네트워크 콜백에서 호출: 현재 wifi 가 집인지 확인 후 start
fun onWifiAvailable(context: Context) {
    getWifiInfo(context)
    val state = _uiState.value
    if (isHomeWifi(state.ssid, state.bssid)) {
        // 집 Wi‑Fi에 연결됨 → 시작 시도
        startHomeLog()
    } else {
        // 집이 아닌 Wi‑Fi에 연결됨 → 진행 중이면 종료
        if (currentLogId != null) endHomeLog()
    }
}

private fun isHomeWifi(currentSsid: String, currentBssid: String): Boolean {
    val hb = homeBssid
    if (!hb.isNullOrBlank() && currentBssid.equals(hb, ignoreCase = true)) return true
    val hs = homeSsid
    return !hs.isNullOrBlank() && currentSsid == hs
}

// 네트워크 콜백에서 호출: Wi‑Fi 해제 시 종료 시도
fun onWifiLost() {
    if (currentLogId != null) {
        endHomeLog()
    }
}

 

그리고 해제에 관련된 로직은 다음과 같다

이후로는 다음의 단계가 있을 것이다

 

1. datastore로 response data 저장 로직(user_id 혹은 log_id)

2. 네트워크가 없을 시 (offline) 네트워크가 연결 되었을때 즉시 log 종료 호출

3. 백그라운드 실행(로그)

4. 통계 분석 API 개발

5. 통계 시각화

6. 클라우드 배포

 

다음의 일정이 남아있다