diff --git a/.github/workflows/integrationtest.yml b/.github/workflows/integrationtest.yml new file mode 100644 index 0000000000..4f7155d9e9 --- /dev/null +++ b/.github/workflows/integrationtest.yml @@ -0,0 +1,61 @@ +name: Android Coverage Report + +on: + workflow_dispatch: # 수동 실행 + push: + branches: [ "iteration-final-test" ] + +jobs: + coverage: + runs-on: ubuntu-latest + defaults: + run: + working-directory: android-studio-app + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Upload coverage.ec + uses: actions/upload-artifact@v4 + with: + name: coverage-ec + path: '**/coverage.ec' + - name: Download coverage.ec + uses: actions/download-artifact@v4 + with: + name: coverage-ec + path: app/ + - name: Check Jacoco report + run: ./gradlew assembleDebug + + # 이미 instrumentation test를 통해 .ec 파일이 생성되어 있다고 가정 + # 여기서는 리포트만 생성 + - name: Generate Jacoco coverage report + run: ./gradlew pixel6DebugAndroidTestCoverage --rerun-tasks + - name: Check Jacoco report + run: | + pwd + ls -R ./ + - name: Upload coverage report artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: /home/runner/work/swpp-2025-project-team-16/swpp-2025-project-team-16/android-studio-app/app/build/reports/jacoco/pixel6DebugAndroidTestCoverage/html + - name: Upload problem report artifact + uses: actions/upload-artifact@v4 + with: + name: problem-report + path: | + /home/runner/work/swpp-2025-project-team-16/swpp-2025-project-team-16/android-studio-app/app/build/reports/jacoco/pixel6DebugAndroidTestCoverage/html + /home/runner/work/swpp-2025-project-team-16/swpp-2025-project-team-16/android-studio-app/app/build/reports/jacoco/pixel6DebugAndroidTestCoverage/*.xml + + + diff --git a/android-studio-app/app/build.gradle.kts b/android-studio-app/app/build.gradle.kts index b32d2b0af4..7ac9d1a2be 100644 --- a/android-studio-app/app/build.gradle.kts +++ b/android-studio-app/app/build.gradle.kts @@ -3,6 +3,9 @@ plugins { id("org.jetbrains.kotlin.android") id("kotlin-parcelize") id("androidx.navigation.safeargs.kotlin") + id("jacoco") + + } android { @@ -20,6 +23,11 @@ android { } buildTypes { + getByName("debug") { + // 최신 AGP에서는 testCoverageEnabled 제거됨 + // rootcoverage 플러그인이 커버리지 수집을 담당 + isTestCoverageEnabled=true + } debug{ isMinifyEnabled=false } @@ -49,43 +57,92 @@ android { } testOptions { + managedDevices { + devices { + // Kotlin DSL에서는 create("이름") 으로 정의 + create("pixel6") { + device = "Pixel 6" + apiLevel = 33 + systemImageSource = "google-atd" + } + } + } + + unitTests { isIncludeAndroidResources = true } } tasks.register("jacocoAndroidTestReport") { - // 기기/에뮬레이터 테스트 실행 후 리포트 생성 - dependsOn("connectedDebugAndroidTest") + dependsOn(":app:connectedDebugAndroidTest") reports { xml.required.set(true) html.required.set(true) } - // 소스 디렉터리 (Java + Kotlin) - sourceDirectories.setFrom( - files("src/main/java", "src/main/kotlin") + val fileFilter = listOf( + "**/R.class", + "**/R$*.class", + "**/BuildConfig.*", + "**/Manifest*.*", + "**/*Test*.*" ) - // 클래스 디렉터리 (AGP 버전에 따라 경로 달라질 수 있음) - classDirectories.setFrom( - fileTree("$buildDir/intermediates/classes/debug") { - exclude( - "**/R.class", "**/R$*.class", - "**/BuildConfig.*", "**/Manifest*.*", - "android/**/*.*" - ) - }, - fileTree("$buildDir/tmp/kotlin-classes/debug") // Kotlin 클래스 - ) + // Kotlin 클래스와 Java 클래스 디렉토리 모두 잡아줍니다 + val kotlinDebugTree = fileTree("${project(":app").buildDir}/tmp/kotlin-classes/debug") { + exclude(fileFilter) + } + val javaDebugTree = fileTree("${project(":app").buildDir}/intermediates/javac/debug/classes") { + exclude(fileFilter) + } - // 실행 데이터 (.ec 파일) - executionData.setFrom( - fileTree("$buildDir/outputs/code_coverage/debugAndroidTest/connected") { - include("*.ec") - } + + val mainSrc = "${project(":app").projectDir}/src/main/java" + + sourceDirectories.setFrom(files(mainSrc)) + classDirectories.setFrom(files(kotlinDebugTree, javaDebugTree)) + + // 여기서 coverage.ec 파일을 읽어옵니다 + executionData.setFrom(fileTree("${project(":app").buildDir}/outputs/code_coverage/debugAndroidTest/connected/SM-S918N - 13") { + include("coverage.ec") + }) + } + + tasks.register("pixel6DebugAndroidTestCoverage") { + reports { + xml.required.set(true) + html.required.set(true) + } + + val fileFilter = listOf( + "**/R.class", + "**/R$*.class", + "**/BuildConfig.*", + "**/Manifest*.*", + "**/*Test*.*" ) + + // Kotlin 클래스와 Java 클래스 디렉토리 모두 잡아줍니다 + val kotlinDebugTree = fileTree("${project(":app").buildDir}/tmp/kotlin-classes/debug") { + exclude(fileFilter) + } + val javaDebugTree = fileTree("${project(":app").buildDir}/intermediates/javac/debug/compileDebugJavaWithJavac/classes") { + exclude(fileFilter) + } + + + val mainSrc = "${project(":app").projectDir}/src/main/java" + + sourceDirectories.setFrom(files(mainSrc)) + classDirectories.setFrom(files(kotlinDebugTree, javaDebugTree)) + + // 여기서 coverage.ec 파일을 읽어옵니다 + executionData.setFrom(fileTree(rootProject.projectDir) { + include("**/coverage.ec") + }) + } } diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeApiService.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeApiService.kt index c573c8b3c0..3af493fcee 100644 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeApiService.kt +++ b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeApiService.kt @@ -11,7 +11,7 @@ import com.fitquest.app.model.login.SignupResponse import okhttp3.ResponseBody.Companion.toResponseBody import retrofit2.Response -abstract class FakeApiService: AuthApiService { +class FakeApiService: AuthApiService { override suspend fun signup(request: SignupRequest): Response { return when (request.name) { "test" -> Response.success(SignupResponse("well","Not Null")) diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeDailySummaryService.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeDailySummaryService.kt new file mode 100644 index 0000000000..cbb79a1f65 --- /dev/null +++ b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeDailySummaryService.kt @@ -0,0 +1,17 @@ +package com.fitquest.app + +import com.fitquest.app.data.remote.DailySummaryApiService +import com.fitquest.app.model.DailySummary +import com.fitquest.app.model.MessageResponse +import org.threeten.bp.LocalDate + +class FakeDailySummaryService: DailySummaryApiService { + override suspend fun getDailySummaries(): List { + return listOf(DailySummary(date= LocalDate.now())) + + } + + override suspend fun autoGenerateDailySummaries(): MessageResponse { + return MessageResponse("well") + } +} \ No newline at end of file diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakePoseAnalysisApiService.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakePoseAnalysisApiService.kt new file mode 100644 index 0000000000..281e66a43a --- /dev/null +++ b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakePoseAnalysisApiService.kt @@ -0,0 +1,14 @@ +package com.fitquest.app + +import com.fitquest.app.data.remote.PoseAnalysisApiService +import com.fitquest.app.model.pose.PoseUploadRequest +import com.fitquest.app.model.pose.PoseUploadResponse +import kotlinx.coroutines.delay +import retrofit2.Response + +class FakePoseAnalysisApiService: PoseAnalysisApiService { + override suspend fun uploadPose(request: PoseUploadRequest): Response { + delay(10000) + return Response.success(PoseUploadResponse(0,"well","poor","do right")) + } +} \ No newline at end of file diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeScheduleApiService.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeScheduleApiService.kt index 0e22748434..ad2cba349c 100644 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeScheduleApiService.kt +++ b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeScheduleApiService.kt @@ -1,21 +1,23 @@ package com.fitquest.app import com.fitquest.app.data.remote.ScheduleApiService +import com.fitquest.app.model.AutoGenerateSchedulesResponse import com.fitquest.app.model.Schedule import org.threeten.bp.LocalDate import org.threeten.bp.LocalTime class FakeScheduleApiService: ScheduleApiService { override suspend fun getSchedules(status: String?): List { - return listOf(Schedule(scheduledDate = LocalDate.now(), startTime = LocalTime.now(), endTime = LocalTime.now(), activity = "squat")) + return listOf(Schedule(scheduledDate = LocalDate.now(), startTime = LocalTime.now(), endTime = LocalTime.now().plusHours(1), activity = "lunge",status="planned", repsTarget = 11, repsDone = 0,id=0,user=0, updatedAt = null, createdAt = null, durationDone = null), + Schedule(scheduledDate = LocalDate.now().minusDays(1), startTime = LocalTime.now(),endTime=LocalTime.now(), activity = "lunge", status = "missed",id=1), + Schedule(scheduledDate = LocalDate.now().minusDays(2), startTime = LocalTime.now(), endTime = LocalTime.now().plusHours(1), activity = "lunge",status="planned", repsTarget = 100, repsDone = 0,id=2), + Schedule(scheduledDate = LocalDate.now().plusDays(1), startTime = LocalTime.now(), endTime = LocalTime.now().plusHours(1), activity = "lunge",status="planned", repsTarget = 11, repsDone = 0,id=3)) } - override suspend fun createSchedule(schedule: Schedule): Schedule { - return Schedule(scheduledDate = LocalDate.now(), startTime = LocalTime.now(), endTime = LocalTime.now(), activity = "squat") - } - override suspend fun autoGenerateSchedules(): List { - return listOf(Schedule(scheduledDate = LocalDate.now(), startTime = LocalTime.now(), endTime = LocalTime.now(), activity = "squat")) + + override suspend fun autoGenerateSchedules(): AutoGenerateSchedulesResponse { + return AutoGenerateSchedulesResponse(1,listOf("2025-12-07","2025-12-08")) } override suspend fun markMissedSchedules() { diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeSessionApiService.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeSessionApiService.kt index 12f3d2d398..a289af6a5c 100644 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeSessionApiService.kt +++ b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeSessionApiService.kt @@ -1,24 +1,27 @@ package com.fitquest.app -import com.fitquest.app.data.remote.EndSessionRequest + import com.fitquest.app.data.remote.SessionApiService -import com.fitquest.app.data.remote.StartSessionRequest +import com.fitquest.app.model.EndSessionRequest import com.fitquest.app.model.Session +import com.fitquest.app.model.StartSessionRequest +import org.threeten.bp.LocalDateTime import retrofit2.Response class FakeSessionApiService: SessionApiService { override suspend fun getSessions(): List { - TODO("Not yet implemented") + return listOf(Session(activity = "squat",id=0, repsCount = 5, createdAt = LocalDateTime.now().minusDays(1))) } override suspend fun startSession(request: StartSessionRequest): Response { - TODO("Not yet implemented") + return Response.success(Session(activity = "squat",id=0)) } override suspend fun endSession( sessionId: Int, request: EndSessionRequest ): Response { - TODO("Not yet implemented") + return Response.success(Session(activity = "squat", repsCount = 5)) } + } \ No newline at end of file diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeUserApiService.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeUserApiService.kt index d349a2a01a..2418fc456c 100644 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeUserApiService.kt +++ b/android-studio-app/app/src/androidTest/java/com/fitquest/app/FakeUserApiService.kt @@ -1,16 +1,17 @@ package com.fitquest.app -import com.fitquest.app.data.remote.RankResponse + import com.fitquest.app.data.remote.UserApiService +import com.fitquest.app.model.RankResponse import com.fitquest.app.model.User import retrofit2.Response class FakeUserApiService: UserApiService { override suspend fun getRankings(): Response> { - TODO("Not yet implemented") + return Response.success(listOf(RankResponse(1,"test",100,1))) } override suspend fun getProfile(): User { - TODO("Not yet implemented") + return User(0,"test","test@example.com") } } \ No newline at end of file diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/LoginActivityTest.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/LoginActivityTest.kt index 8ce187b4e1..0a62440859 100644 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/LoginActivityTest.kt +++ b/android-studio-app/app/src/androidTest/java/com/fitquest/app/LoginActivityTest.kt @@ -12,12 +12,13 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import androidx.test.rule.GrantPermissionRule -import com.fitquest.app.data.remote.ApiService -import com.fitquest.app.data.remote.EmailCheckResponse -import com.fitquest.app.data.remote.LoginRequest -import com.fitquest.app.data.remote.LoginResponse +import com.fitquest.app.data.remote.AuthApiService +import com.fitquest.app.data.remote.DailySummaryApiService +import com.fitquest.app.data.remote.PoseAnalysisApiService import com.fitquest.app.data.remote.ScheduleApiService import com.fitquest.app.data.remote.ServiceLocator +import com.fitquest.app.data.remote.SessionApiService +import com.fitquest.app.data.remote.UserApiService import com.fitquest.app.model.Schedule import com.fitquest.app.ui.fragments.login.LoginEmailFragment //import kotlinx.coroutines.test.runTest @@ -46,17 +47,25 @@ class LoginActivityTest { android.Manifest.permission.CAMERA ) - private val mockApiService: ApiService= FakeApiService() - private val mockScheduleApiService: ScheduleApiService= FakeScheduleApiService() + private val mockApiService: AuthApiService = FakeApiService() + private val mockScheduleApiService: ScheduleApiService = FakeScheduleApiService() + private val mockSessionApiService: SessionApiService = FakeSessionApiService() + private val mockUserApiService: UserApiService = FakeUserApiService() + private val mockDailySummaryApiService: DailySummaryApiService = FakeDailySummaryService() + private val mockPoseAnalysisApiService: PoseAnalysisApiService =FakePoseAnalysisApiService() @Before fun setUp(){ - ServiceLocator.apiService=mockScheduleApiService - ServiceLocator.authApiService=mockApiService + ServiceLocator.authApiService = mockApiService + ServiceLocator.scheduleApiService = mockScheduleApiService + ServiceLocator.sessionApiService = mockSessionApiService + ServiceLocator.userApiService = mockUserApiService + ServiceLocator.dailySummaryApiService=mockDailySummaryApiService + ServiceLocator.poseAnalysisApiService=mockPoseAnalysisApiService } @Test - fun test_testScenario() { + fun test_admin() { //이메일 입력 onView(withId(R.id.btnEmailCheckQuest)).perform(click()) @@ -74,41 +83,39 @@ class LoginActivityTest { //비밀번호 입력 성공 onView(withId(R.id.etPassword)).perform(typeText("0000"), closeSoftKeyboard()) onView(withId(R.id.btnPasswordLogin)).perform(click()) - - } @Test - fun test_LoginEmailFragment_UI_components_are_displayed() { - onView(withId(R.id.etEmail)).perform(typeText("snu@snu.com")) + fun test_preregistered() { + onView(withId(R.id.etEmail)).perform(typeText("snu@snu.com"),closeSoftKeyboard()) onView(withId(R.id.btnEmailCheckQuest)).perform(click()) - onView(withId(R.id.etPassword)).perform(typeText("0001")) + onView(withId(R.id.etPassword)).perform(typeText("0001"),closeSoftKeyboard()) onView(withId(R.id.btnPasswordLogin)).perform(click()) - onView(withId(R.id.etPassword)).perform(typeText("0000")) + onView(withId(R.id.etPassword)).perform(typeText("0000"),closeSoftKeyboard()) onView(withId(R.id.btnPasswordLogin)).perform(click()) } @Test - fun test_navigateToPasswordStep_when_email_exists() { + fun test_login_error() { - onView(withId(R.id.etEmail)).perform(typeText("snu@snu.com")) + onView(withId(R.id.etEmail)).perform(typeText("snu@snu.com"),closeSoftKeyboard()) onView(withId(R.id.btnEmailCheckQuest)).perform(click()) - onView(withId(R.id.etPassword)).perform(typeText("error")) + onView(withId(R.id.etPassword)).perform(typeText("error"),closeSoftKeyboard()) onView(withId(R.id.btnPasswordLogin)).perform(click()) } @Test - fun test_navigateToPasswordStep_when_email_does_not_exist() { + fun test_email_error() { - onView(withId(R.id.etEmail)).perform(typeText("error@error.com")) + onView(withId(R.id.etEmail)).perform(typeText("error@error.com"),closeSoftKeyboard()) onView(withId(R.id.btnEmailCheckQuest)).perform(click()) } @Test - fun test_navigateToSignUpStep_when_email_does_not_exist() { + fun test_signup() { onView(withId(R.id.etEmail)).perform(typeText("signup@signup.com"), closeSoftKeyboard()) onView(withId(R.id.btnEmailCheckQuest)).perform(click()) @@ -130,23 +137,25 @@ class LoginActivityTest { onView(withId(R.id.etConfirmPassword)).perform(typeText("123456"), closeSoftKeyboard()) //정상 onView(withId(R.id.btnContinue)).perform(click()) + onView(withId(R.id.btnSwitchCamera)).perform(click()) + Thread.sleep(15000) onView(withId(R.id.btnStop)).perform(click()) } @Test - fun test_navigateToSignUpStep_when_error() { + fun test_signup_error() { - onView(withId(R.id.etEmail)).perform(typeText("signup@signup.com")) + onView(withId(R.id.etEmail)).perform(typeText("signup@signup.com"),closeSoftKeyboard()) onView(withId(R.id.btnEmailCheckQuest)).perform(click()) - onView(withId(R.id.etHeroName)).perform(typeText("error")) + onView(withId(R.id.etHeroName)).perform(typeText("error"),closeSoftKeyboard()) // password 입력 - onView(withId(R.id.etPassword)).perform(typeText("123456")) - onView(withId(R.id.etConfirmPassword)).perform(typeText("123456")) + onView(withId(R.id.etPassword)).perform(typeText("123456"),closeSoftKeyboard()) + onView(withId(R.id.etConfirmPassword)).perform(typeText("123456"),closeSoftKeyboard()) //정상 onView(withId(R.id.btnContinue)).perform(click()) - onView(withId(R.id.etHeroName)).perform(typeText("else")) + onView(withId(R.id.etHeroName)).perform(typeText("else"),closeSoftKeyboard()) onView(withId(R.id.btnContinue)).perform(click()) } diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/MainActivityTest.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/MainActivityTest.kt index 0ca206d32f..f3ae08b50c 100644 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/MainActivityTest.kt +++ b/android-studio-app/app/src/androidTest/java/com/fitquest/app/MainActivityTest.kt @@ -1,23 +1,46 @@ package com.fitquest.app +import android.view.View +import androidx.annotation.IdRes +import androidx.navigation.Navigation +import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.pressBack +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.NavigationViewActions +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withSpinnerText +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.filters.MediumTest import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner import androidx.test.rule.GrantPermissionRule -import com.fitquest.app.data.remote.ApiService +import com.fitquest.app.data.remote.AuthApiService +import com.fitquest.app.data.remote.DailySummaryApiService +import com.fitquest.app.data.remote.PoseAnalysisApiService import com.fitquest.app.data.remote.ScheduleApiService import com.fitquest.app.data.remote.ServiceLocator import com.fitquest.app.data.remote.SessionApiService import com.fitquest.app.data.remote.UserApiService +import com.google.android.material.bottomnavigation.BottomNavigationView +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.Matcher +import org.hamcrest.Matchers.containsString +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +@MediumTest @RunWith(AndroidJUnit4ClassRunner::class) class MainActivityTest { @get:Rule @@ -28,19 +51,146 @@ class MainActivityTest { android.Manifest.permission.CAMERA ) - private val mockApiService: ApiService = FakeApiService() + private val mockApiService: AuthApiService = FakeApiService() private val mockScheduleApiService: ScheduleApiService = FakeScheduleApiService() private val mockSessionApiService: SessionApiService = FakeSessionApiService() private val mockUserApiService: UserApiService = FakeUserApiService() + private val mockDailySummaryApiService: DailySummaryApiService = FakeDailySummaryService() + private val mockPoseAnalysisApiService: PoseAnalysisApiService =FakePoseAnalysisApiService() + + @Before fun setUp() { ServiceLocator.authApiService = mockApiService - ServiceLocator.apiService = mockScheduleApiService + ServiceLocator.scheduleApiService = mockScheduleApiService ServiceLocator.sessionApiService = mockSessionApiService ServiceLocator.userApiService = mockUserApiService + ServiceLocator.dailySummaryApiService=mockDailySummaryApiService + ServiceLocator.poseAnalysisApiService=mockPoseAnalysisApiService + + } + fun clickBottomNavigationItem(@IdRes itemId: Int): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher { + return isAssignableFrom(BottomNavigationView::class.java) + } + + override fun getDescription(): String { + return "Click on BottomNavigationView item" + } + + override fun perform(uiController: UiController?, view: View?) { + val bottomNavigationView = view as BottomNavigationView + bottomNavigationView.selectedItemId = itemId + } + } } + @Test fun test_activity_in_view() { - onView(withId(R.id.mainFragmentContainer)).check(matches(isDisplayed())) + scenarioRule.scenario.onActivity { activity -> + // NavController 가져오기 + val navController = Navigation.findNavController(activity, R.id.mainFragmentContainer) + + // NavController가 attach 되었는지 확인 + assertNotNull(navController) + + // 현재 목적지가 nav_graph의 startDestination인지 검증 + val expectedStartDest = navController.graph.startDestinationId + assertEquals(expectedStartDest, navController.currentDestination?.id) + } + } + @Test + fun test_journey_fragment(){ + onView(withId(R.id.bottomNavigation)) + .perform(clickBottomNavigationItem(R.id.journeyFragment)) + onView(withId(R.id.recyclerJourney)) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 0, click() + ) + ) + onView(withId(R.id.btnStartSession)).perform(click()) + onView(withId(R.id.btnStartWorkout)).perform(click()) + Thread.sleep(35000) + onView(withId(R.id.btnSwitchCamera)).perform(click()) + onView(withId(R.id.btnStartWorkout)).perform(click()) + } + @Test + fun test_aiCoachFragment(){ + onView(withId(R.id.bottomNavigation)) + .perform(clickBottomNavigationItem(R.id.aiCoachFragment)) + onView(withId(R.id.btnStartWorkout)).perform(click()) + onView(withId(R.id.btnStartWorkout)).perform(click()) + onView(withId(R.id.btnStartWorkout)).perform(click()) + onView(withId(R.id.btnSwitchCamera)).perform(click()) + Thread.sleep(15000) + onView(withId(R.id.btnStartWorkout)).perform(click()) + onView(withId(R.id.btnLater)).perform(click()) + onView(withId(R.id.spinnerExercise)).perform(click()) + // 2. 드롭다운에서 "Push-ups" 항목 클릭 + onView(withText(containsString("Plank"))).perform(click()) + onView(withId(R.id.btnStartWorkout)).perform(click()) + onView(withId(R.id.btnSwitchCamera)).perform(click()) + onView(withId(R.id.btnSwitchCamera)).perform(click()) + Thread.sleep(15000) + onView(withId(R.id.btnStartWorkout)).perform(click()) + onView(withId(R.id.btnLater)).perform(click()) + + onView(withId(R.id.spinnerExercise)).perform(click()) + // 2. 드롭다운에서 "Push-ups" 항목 클릭 + onView(withText(containsString("Lunge"))).perform(click()) + onView(withId(R.id.btnStartWorkout)).perform(click()) + Thread.sleep(15000) + onView(withId(R.id.btnStartWorkout)).perform(click()) + Thread.sleep(5000) + onView(withId(R.id.btnEvaluate)).perform(click()) + onView(withId(R.id.bottomNavigation)) + .perform(clickBottomNavigationItem(R.id.profileFragment)) + onView(withId(R.id.recyclerHistory)) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 0, click() + ) + ) + + + } + @Test + fun test_PoseFragment(){ + onView(withId(R.id.bottomNavigation)) + .perform(clickBottomNavigationItem(R.id.poseFragment)) + onView(withId(R.id.spinnerExercisePose)).perform(click()) + + // 2. 드롭다운에서 "Push-ups" 항목 클릭 + onView(withText(containsString("Plank"))).perform(click()) + + // 3. Spinner에 선택된 값이 표시되는지 확인 + onView(allOf(withId(R.id.spinnerExercisePose), withSpinnerText(containsString("Plank")))) + .check(matches(isDisplayed())) + onView(withId(R.id.spinnerExercisePose)).perform(click()) + // 2. 드롭다운에서 "Push-ups" 항목 클릭 + onView(withText(containsString("Squat"))).perform(click()) + onView(withId(R.id.btnSwitchCamera)).perform(click()) + onView(withId(R.id.btnSwitchCamera)).perform(click()) + onView(withId(R.id.btnCapture)).perform(click()) + Thread.sleep(25000) + onView(withId(R.id.btnResultClose)).perform(click()) + onView(withId(R.id.btnUpload)).perform(click()) + + } + @Test + fun test_ProfileFragment(){ + onView(withId(R.id.bottomNavigation)) + .perform(clickBottomNavigationItem(R.id.profileFragment)) + onView(withId(R.id.btnViewRankings)).perform(click()) + onView(withId(R.id.btnCloseRank)).perform(click()) + onView(withId(R.id.recyclerHistory)) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 0, click() + ) + ) + pressBack() } } \ No newline at end of file diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/AiCoachFragmentTest.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/AiCoachFragmentTest.kt deleted file mode 100644 index e6623dce03..0000000000 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/AiCoachFragmentTest.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.fitquest.app.ui.fragments - -import android.os.Bundle -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.test.espresso.Espresso.onData -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.* -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fitquest.app.R -import com.fitquest.app.ui.fragments.coach.AiCoachFragment -import org.hamcrest.Matchers.allOf -import org.hamcrest.Matchers.instanceOf -import org.hamcrest.Matchers.`is` -import org.hamcrest.Matchers.not -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class AiCoachFragmentTest { - - @Test - fun test_initial_ui_state_is_correct() { - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - - // Check that initial controls are visible - onView(withId(R.id.spinnerExercise)).check(matches(isDisplayed())) - onView(withId(R.id.btnStartWorkout)).check(matches(isDisplayed())) - onView(withId(R.id.btnSwitchCamera)).check(matches(isDisplayed())) - - // Check that workout-in-progress HUD elements are not visible - onView(withId(R.id.hudTopContainer)).check(matches(not(isDisplayed()))) - onView(withId(R.id.recordingIndicator)).check(matches(not(isDisplayed()))) - onView(withId(R.id.formScoreContainer)).check(matches(not(isDisplayed()))) - onView(withId(R.id.overlay)).check(matches(not(isDisplayed()))) - - // Check initial text on buttons and labels - onView(withId(R.id.btnStartWorkout)).check(matches(withText("Begin Training"))) - onView(withId(R.id.tvRepCount)).check(matches(withText("0"))) - onView(withId(R.id.tvFeedback)).check(matches(withText("Position yourself in frame"))) - } - - @Test - fun test_start_button_initiates_countdown() { - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - - onView(withId(R.id.btnStartWorkout)).perform(click()) - - // Countdown UI should appear - onView(withId(R.id.tvCountdown)).check(matches(isDisplayed())) - onView(withId(R.id.tvFeedback)).check(matches(withText("Get ready... ⏳"))) - // In a real test, we'd use an IdlingResource to wait for the countdown to finish - // and then verify the transition to the 'training' state. - } - - @Test - fun test_cancel_countdown_returns_to_idle() { - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - - // Start countdown - onView(withId(R.id.btnStartWorkout)).perform(click()) - onView(withId(R.id.tvCountdown)).check(matches(isDisplayed())) - - // Click again to cancel - onView(withId(R.id.btnStartWorkout)).perform(click()) - - // Check if UI returned to idle state - onView(withId(R.id.tvCountdown)).check(matches(not(isDisplayed()))) - onView(withId(R.id.btnStartWorkout)).check(matches(withText("Begin Training"))) - onView(withId(R.id.tvFeedback)).check(matches(withText("Position yourself in frame"))) - } - - @Test - fun test_exercise_spinner_selection_updates_ui() { - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - - // Select "Plank" from the spinner - onView(withId(R.id.spinnerExercise)).perform(click()) - onData(allOf(instanceOf(String::class.java), `is`("⏱️ Plank"))).perform(click()) - - // Check if the UI updated for a time-based exercise - onView(withId(R.id.labelReps)).check(matches(withText("SECONDS"))) - onView(withId(R.id.tvRepCount)).check(matches(withText("0.0"))) - onView(withId(R.id.tvCurrentExerciseEmoji)).check(matches(withText("⏱️"))) - } - - @Test - fun test_switch_camera_button_is_clickable() { - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - - onView(withId(R.id.btnSwitchCamera)) - .check(matches(isDisplayed())) - .perform(click()) - } - - @Test - fun test_ui_for_scheduled_workout_is_locked() { - val args = Bundle().apply { - putInt("scheduleId", 1) - putString("activityKey", "lunge") - putInt("repsTarget", 15) - } - launchFragmentInContainer(fragmentArgs = args, themeResId = R.style.Theme_FitQuest) - - // Spinner should be disabled - onView(withId(R.id.spinnerExercise)).check(matches(not(isEnabled()))) - - // Subtitle should show schedule info - onView(withId(R.id.tvSystemSubtitle)).check(matches(withText("Lunge Target: 15 Reps"))) - - // Emoji and label should match the scheduled exercise - onView(withId(R.id.tvCurrentExerciseEmoji)).check(matches(withText("🦵"))) - onView(withId(R.id.labelReps)).check(matches(withText("REPS"))) - } -} diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/JourneyFragmentTest.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/JourneyFragmentTest.kt deleted file mode 100644 index 3f4d8d8d9f..0000000000 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/JourneyFragmentTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.fitquest.app.ui.fragments - -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions -import androidx.test.espresso.matcher.ViewMatchers.* -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fitquest.app.R -import org.hamcrest.Matchers.greaterThan -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class JourneyFragmentTest { - - @Test - fun test_initial_ui_is_displayed() { - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - - onView(withId(R.id.headerContainer)).check(matches(isDisplayed())) - onView(withId(R.id.resultScroll)).check(matches(isDisplayed())) - onView(withId(R.id.timelineVerticalLine)).check(matches(isDisplayed())) - } - - @Test - fun test_timeline_is_populated_after_fetch() { - // This test requires mocking the network call to provide data. - // For now, we assume that if the call is successful, views will be added. - // A more robust test would use an IdlingResource and a mock web server. - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - - // After data is fetched and populateTimeline() is called, the container should have children. - // We can check if the timelineContainer has any views. - // onView(withId(R.id.timelineContainer)).check(matches(hasMinimumChildCount(1))) - } - - @Test - fun test_clicking_timeline_node_opens_bottom_sheet() { - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - - // This requires data to be populated first. Assuming there is at least one item. - // We'd click on the first available card. The ID is inside a dynamically added layout. - // onView(firstChildOf(withId(R.id.timelineContainer))).perform(click()) - // Then check if the bottom sheet is displayed. - // onView(withId(R.id.layout_journey_daydetail)).check(matches(isDisplayed())) - } - - @Test - fun test_bottom_sheet_displays_correct_data() { - // Similar to the above, this would require populating data and clicking an item. - // Then, we would check the content of the views in the bottom sheet. - // onView(withId(R.id.tvDayTitle)).check(matches(withText("Expected Date"))) - // onView(withId(R.id.exerciseListContainer)).check(matches(hasMinimumChildCount(1))) - } - - @Test - fun test_close_button_dismisses_bottom_sheet() { - // 1. Launch fragment and populate data - // 2. Click a node to show the bottom sheet - // 3. Click the close button - // onView(withId(R.id.btnClose)).perform(click()) - // 4. Verify the bottom sheet is no longer displayed - // onView(withId(R.id.layout_journey_daydetail)).check(doesNotExist()) - } -} diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/PoseFragmentTest.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/PoseFragmentTest.kt deleted file mode 100644 index 82fd7d4440..0000000000 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/PoseFragmentTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.fitquest.app.ui.fragments - -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.* -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fitquest.app.R -import com.fitquest.app.ui.fragments.pose.PoseFragment -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class PoseFragmentTest { - - @Before - fun setup() { - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - } - - @Test - fun test_initial_ui_is_displayed() { - // Check headers - onView(withId(R.id.tvSystemTitle)).check(matches(withText("POSE ESTIMATION"))) - onView(withId(R.id.tvSystemSubtitle)).check(matches(withText("Evaluate your pose"))) - - // Check camera and control buttons - onView(withId(R.id.cameraPreview)).check(matches(isDisplayed())) - onView(withId(R.id.btnSwitchCamera)).check(matches(isDisplayed())) - onView(withId(R.id.btnCapture)).check(matches(isDisplayed())) - onView(withId(R.id.btnUpload)).check(matches(isDisplayed())) - - // Check initial guide text is visible - onView(withId(R.id.tvGuideText)).check(matches(isDisplayed())) - onView(withId(R.id.tvGuideText)).check(matches(withText("Take a picture or upload from gallery"))) - - // Check feedback card is visible - - } - - @Test - fun test_capture_button_is_clickable() { - onView(withId(R.id.btnCapture)).perform(click()) - // In a real test, we would use Espresso-Intents to verify that the camera intent is fired. - } - - @Test - fun test_upload_button_is_clickable() { - onView(withId(R.id.btnUpload)).perform(click()) - // Similarly, use Espresso-Intents to verify the gallery intent. - } - - @Test - fun test_switch_camera_button_is_clickable() { - onView(withId(R.id.btnSwitchCamera)).perform(click()) - // This should switch the camera preview, which can be hard to verify in a test - // without more complex setup. - } - - -} diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/ProfileFragmentTest.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/ProfileFragmentTest.kt deleted file mode 100644 index 3a9c0f4886..0000000000 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/ProfileFragmentTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.fitquest.app.ui.fragments - -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.* -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fitquest.app.R -import org.hamcrest.Matchers.not -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ProfileFragmentTest { - - @Before - fun setup() { - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - } - - @Test - fun test_initial_ui_is_displayed() { - // Check headers and titles - onView(withId(R.id.tvProfileTitle)).check(matches(withText("PERSONAL PROFILE"))) - onView(withId(R.id.tvHistoryTitle)).check(matches(withText("History"))) - - // Check main components - onView(withId(R.id.statsContainer)).check(matches(isDisplayed())) - onView(withId(R.id.btnViewRankings)).check(matches(isDisplayed())) - onView(withId(R.id.historyFrame)).check(matches(isDisplayed())) - - // Rank overlay should be hidden initially - onView(withId(R.id.rankOverlay)).check(matches(not(isDisplayed()))) - } - - @Test - fun test_view_rankings_button_shows_overlay() { - onView(withId(R.id.btnViewRankings)).perform(click()) - onView(withId(R.id.rankOverlay)).check(matches(isDisplayed())) - - // Check some elements inside the rank overlay - onView(withId(R.id.tvTitle)).check(matches(withText("🏆 TOP RANKINGS"))) - onView(withId(R.id.podiumContainer)).check(matches(isDisplayed())) - } - - @Test - fun test_close_rankings_button_hides_overlay() { - // First, open the rank overlay - onView(withId(R.id.btnViewRankings)).perform(click()) - onView(withId(R.id.rankOverlay)).check(matches(isDisplayed())) - - // Then, click the close button - onView(withId(R.id.btnCloseRank)).perform(click()) - - // Verify the overlay is hidden - // We might need an IdlingResource for the animation, but for now, this might work if the view is set to GONE. - // A more robust way is to check for `doesNotExist()` after a short delay. - } - - @Test - fun test_history_is_populated() { - // This test requires mocking network calls. - // Assuming data is fetched, the historyContainer should have children. - // onView(withId(R.id.historyContainer)).check(matches(hasMinimumChildCount(1))) - } - - @Test - fun test_clicking_history_node_opens_bottom_sheet() { - // This requires data to be populated first. - // 1. Mock data and ensure history is populated. - // 2. Click on a history item. - // 3. Verify the bottom sheet (layout_history_detail) is displayed. - // onView(withId(R.id.layout_history_detail)).check(matches(isDisplayed())) - } -} diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/LoginEmailFragmentTest.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/LoginEmailFragmentTest.kt deleted file mode 100644 index 5ccec7a04b..0000000000 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/LoginEmailFragmentTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.fitquest.app.ui.fragments.login - -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.typeText -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fitquest.app.R -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class LoginEmailFragmentTest { - - @Before - fun setup() { - launchFragmentInContainer(themeResId = R.style.Theme_FitQuest) - } - - @Test - fun test_all_fields_are_displayed() { - onView(withId(R.id.etEmail)).check(matches(isDisplayed())) - onView(withId(R.id.btnEmailCheckQuest)).check(matches(isDisplayed())) - } - - @Test - fun test_fill_form_and_click_continue() { - onView(withId(R.id.etEmail)).perform(typeText("test@example.com")) - onView(withId(R.id.btnEmailCheckQuest)).perform(click()) - // Add assertion to check navigation - } - - @Test - fun test_empty_email_shows_error() { - onView(withId(R.id.btnEmailCheckQuest)).perform(click()) - onView(withId(R.id.etEmail)).check(matches(isDisplayed())) - } - - @Test - fun test_email_format_is_validated() { - onView(withId(R.id.etEmail)).perform(typeText("invalid-email")) - onView(withId(R.id.btnEmailCheckQuest)).perform(click()) - // Add assertion to check for email format validation - } -} diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/LoginPasswordFragmentTest.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/LoginPasswordFragmentTest.kt deleted file mode 100644 index 9b95714c8d..0000000000 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/LoginPasswordFragmentTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.fitquest.app.ui.fragments.login - -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.typeText -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fitquest.app.R -import com.fitquest.app.data.remote.ApiService -import com.fitquest.app.data.remote.ServiceLocator -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -@RunWith(AndroidJUnit4::class) -class LoginPasswordFragmentTest { - - private val testEmail = "test@example.com" - @Mock - private lateinit var mockApiService: ApiService - - @Before - fun setup() { - MockitoAnnotations.openMocks(this) - val fragmentArgs = LoginPasswordFragment.newInstance(testEmail).arguments - launchFragmentInContainer(fragmentArgs, R.style.Theme_FitQuest) - } - - @Test - fun test_all_fields_are_displayed() { - onView(withId(R.id.tvEmail)).check(matches(isDisplayed())) - onView(withId(R.id.etPassword)).check(matches(isDisplayed())) - onView(withId(R.id.btnPasswordLogin)).check(matches(isDisplayed())) - onView(withId(R.id.btnBack)).check(matches(isDisplayed())) - } - - @Test - fun test_password_is_required() { - onView(withId(R.id.btnPasswordLogin)).perform(click()) - onView(withId(R.id.etPassword)).check(matches(isDisplayed())) // Placeholder, should check for error - } - - @Test - fun test_back_button_works() { - onView(withId(R.id.btnBack)).perform(click()) - // Add assertion for navigation back - } - - @Test - fun test_login_with_valid_credentials() { - onView(withId(R.id.etPassword)).perform(typeText("password")) - onView(withId(R.id.btnPasswordLogin)).perform(click()) - // Add assertion for successful login - } - - @Test - fun test_login_with_invalid_credentials() { - onView(withId(R.id.etPassword)).perform(typeText("wrong_password")) - onView(withId(R.id.btnPasswordLogin)).perform(click()) - // Add assertion to check for error message - } -} diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/SignupStep1FragmentTest.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/SignupStep1FragmentTest.kt deleted file mode 100644 index 79ef76f9dc..0000000000 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/SignupStep1FragmentTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.fitquest.app.ui.fragments.login - -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.typeText -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fitquest.app.R -import com.fitquest.app.data.remote.ApiService -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -@RunWith(AndroidJUnit4::class) -class SignupStep1FragmentTest { - - private val testEmail = "test@example.com" - @Mock - private lateinit var mockApiService: ApiService - @Before - fun setup() { - - MockitoAnnotations.openMocks(this) - val fragmentArgs = SignupStep1Fragment.newInstance(testEmail).arguments - launchFragmentInContainer(fragmentArgs, R.style.Theme_FitQuest) - } - - @Test - fun test_all_fields_are_displayed() { - onView(withId(R.id.etEmail)).check(matches(isDisplayed())) - onView(withId(R.id.etHeroName)).check(matches(isDisplayed())) - onView(withId(R.id.etPassword)).check(matches(isDisplayed())) - onView(withId(R.id.etConfirmPassword)).check(matches(isDisplayed())) - onView(withId(R.id.btnContinue)).check(matches(isDisplayed())) - onView(withId(R.id.btnBack)).check(matches(isDisplayed())) - } - - @Test - fun test_empty_fields_show_error() { - onView(withId(R.id.btnContinue)).perform(click()) - // Add assertions for error messages - } - - @Test - fun test_password_mismatch_shows_error() { - onView(withId(R.id.etEmail)).perform(typeText(testEmail)) - onView(withId(R.id.etHeroName)).perform(typeText("TestUser")) - onView(withId(R.id.etPassword)).perform(typeText("password123")) - onView(withId(R.id.etConfirmPassword)).perform(typeText("password456")) - onView(withId(R.id.btnContinue)).perform(click()) - // Add assertion for password mismatch error - } - - @Test - fun test_short_password_shows_error() { - onView(withId(R.id.etEmail)).perform(typeText(testEmail)) - onView(withId(R.id.etHeroName)).perform(typeText("TestUser")) - onView(withId(R.id.etPassword)).perform(typeText("123")) - onView(withId(R.id.etConfirmPassword)).perform(typeText("123")) - onView(withId(R.id.btnContinue)).perform(click()) - // Add assertion for short password error - } - - @Test - fun test_back_button_works() { - onView(withId(R.id.btnBack)).perform(click()) - // Add assertion for navigation back - } - - @Test - fun test_successful_signup_navigates_to_next_step() { - onView(withId(R.id.etEmail)).perform(typeText(testEmail)) - onView(withId(R.id.etHeroName)).perform(typeText("TestUser")) - onView(withId(R.id.etPassword)).perform(typeText("password123")) - onView(withId(R.id.etConfirmPassword)).perform(typeText("password123")) - onView(withId(R.id.btnContinue)).perform(click()) - // Add assertion for navigation to the next step - } -} diff --git a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/SignupStep2FragmentTest.kt b/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/SignupStep2FragmentTest.kt deleted file mode 100644 index bf4d7f506a..0000000000 --- a/android-studio-app/app/src/androidTest/java/com/fitquest/app/ui/fragments/login/SignupStep2FragmentTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.fitquest.app.ui.fragments.login - -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fitquest.app.R -import com.fitquest.app.data.remote.ApiService -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -@RunWith(AndroidJUnit4::class) -class SignupStep2FragmentTest { - @Mock - private lateinit var mockApiService: ApiService - - @Before - fun setup() { - MockitoAnnotations.openMocks(this) - val fragmentArgs = SignupStep2Fragment.newInstance("test@example.com", "password", "TestUser").arguments - launchFragmentInContainer(fragmentArgs, R.style.Theme_FitQuest) - } - - @Test - fun test_all_views_are_displayed() { - onView(withId(R.id.cameraPreview)).check(matches(isDisplayed())) - onView(withId(R.id.tvCountdown)).check(matches(isDisplayed())) - onView(withId(R.id.tvCountNumber)).check(matches(isDisplayed())) - onView(withId(R.id.btnStop)).check(matches(isDisplayed())) - } - - @Test - fun test_countdown_is_visible_on_start() { - // The countdown TextView is initially visible and starts automatically. - onView(withId(R.id.tvCountdown)).check(matches(isDisplayed())) - // To test the countdown itself, IdlingResource would be needed. - } - - @Test - fun test_initial_count_is_zero() { - onView(withId(R.id.tvCountNumber)).check(matches(withText("0"))) - } - - @Test - fun test_stop_button_is_clickable() { - onView(withId(R.id.btnStop)).perform(click()) - // Add assertion for stopping the session and navigating away - } - - @Test - fun test_camera_permission_is_handled() { - // This test requires a more complex setup to deny permissions and check for the request. - // For now, we assume permissions are granted for other tests. - } -} diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/MainActivity.kt b/android-studio-app/app/src/main/java/com/fitquest/app/MainActivity.kt index ac6cb43c18..bfb7842c7d 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/MainActivity.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/MainActivity.kt @@ -11,6 +11,7 @@ import androidx.core.view.forEach import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import com.fitquest.app.data.remote.RetrofitClient +import com.fitquest.app.data.remote.ServiceLocator import com.fitquest.app.databinding.ActivityMainBinding import com.fitquest.app.model.InitProgress import com.fitquest.app.ui.viewmodels.AiCoachViewModel @@ -24,13 +25,13 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val mainActivityViewModel: MainActivityViewModel by viewModels { MainActivityViewModelFactory( - RetrofitClient.scheduleApiService, - RetrofitClient.dailySummaryApiService + ServiceLocator.scheduleApiService, + ServiceLocator.dailySummaryApiService ) } private val coachVm: AiCoachViewModel by viewModels { - AiCoachViewModelFactory(RetrofitClient.sessionApiService) + AiCoachViewModelFactory(ServiceLocator.sessionApiService) } private val navController by lazy { diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/data/remote/ServiceLocator.kt b/android-studio-app/app/src/main/java/com/fitquest/app/data/remote/ServiceLocator.kt index 8d5ea4c137..a2891b2be0 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/data/remote/ServiceLocator.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/data/remote/ServiceLocator.kt @@ -1,10 +1,18 @@ package com.fitquest.app.data.remote object ServiceLocator { - var apiService: ScheduleApiService = RetrofitClient.scheduleApiService + var scheduleApiService: ScheduleApiService = RetrofitClient.scheduleApiService var sessionApiService: SessionApiService= RetrofitClient.sessionApiService var userApiService: UserApiService= RetrofitClient.userApiService var authApiService: AuthApiService= RetrofitClient.authApiService + var dailySummaryApiService: DailySummaryApiService = RetrofitClient.dailySummaryApiService + var poseAnalysisApiService: PoseAnalysisApiService = RetrofitClient.poseAnalysisApiService + + + + + + diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/ui/coachutils/PoseLandmarkerHelper.kt b/android-studio-app/app/src/main/java/com/fitquest/app/ui/coachutils/PoseLandmarkerHelper.kt index 0fe79f8d97..489716028f 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/ui/coachutils/PoseLandmarkerHelper.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/ui/coachutils/PoseLandmarkerHelper.kt @@ -3,8 +3,6 @@ package com.fitquest.app.ui.coachutils import android.content.Context import android.graphics.Bitmap import android.graphics.Matrix -import android.media.MediaMetadataRetriever -import android.net.Uri import android.os.SystemClock import android.util.Log import androidx.annotation.VisibleForTesting @@ -48,10 +46,6 @@ class PoseLandmarkerHelper( poseLandmarker = null } - fun isClose(): Boolean { - return poseLandmarker == null - } - /** * MediaPipe PoseLandmarker 인스턴스를 초기화한다. * GPU delegate를 쓰는 경우, 반드시 그 delegate를 초기화한 thread(=우리가 analyzer 돌리는 executor thread)에서 호출해야 한다. @@ -174,112 +168,6 @@ class PoseLandmarkerHelper( // 비디오 파일, 스틸 이미지용 detectVideoFile(), detectImage() 등은 // 원본 그대로 유지. 필요 없으면 나중에 정리해도 됨. - fun detectVideoFile( - videoUri: Uri, - inferenceIntervalMs: Long - ): ResultBundle? { - if (runningMode != RunningMode.VIDEO) { - throw IllegalArgumentException( - "detectVideoFile() called while not in VIDEO mode." - ) - } - - val startTime = SystemClock.uptimeMillis() - - var didErrorOccurred = false - - val retriever = MediaMetadataRetriever() - retriever.setDataSource(context, videoUri) - val videoLengthMs = - retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) - ?.toLong() - - val firstFrame = retriever.getFrameAtTime(0) - val width = firstFrame?.width - val height = firstFrame?.height - - if ((videoLengthMs == null) || (width == null) || (height == null)) return null - - val resultList = mutableListOf() - val numberOfFrameToRead = videoLengthMs.div(inferenceIntervalMs) - - for (i in 0..numberOfFrameToRead) { - val timestampMs = i * inferenceIntervalMs - - retriever - .getFrameAtTime( - timestampMs * 1000, - MediaMetadataRetriever.OPTION_CLOSEST - ) - ?.let { frame -> - val argb8888Frame = - if (frame.config == Bitmap.Config.ARGB_8888) frame - else frame.copy(Bitmap.Config.ARGB_8888, false) - - val mpImage = BitmapImageBuilder(argb8888Frame).build() - - poseLandmarker?.detectForVideo(mpImage, timestampMs) - ?.let { detectionResult -> - resultList.add(detectionResult) - } ?: run { - didErrorOccurred = true - poseLandmarkerHelperListener?.onError( - "ResultBundle could not be returned in detectVideoFile" - ) - } - } - ?: run { - didErrorOccurred = true - poseLandmarkerHelperListener?.onError( - "Frame could not be retrieved when detecting in video." - ) - } - } - - retriever.release() - - val inferenceTimePerFrameMs = - (SystemClock.uptimeMillis() - startTime).div(numberOfFrameToRead) - - return if (didErrorOccurred) { - null - } else { - ResultBundle( - resultList, - inferenceTimePerFrameMs, - height, - width - ) - } - } - - fun detectImage(image: Bitmap): ResultBundle? { - if (runningMode != RunningMode.IMAGE) { - throw IllegalArgumentException( - "detectImage() called while not in IMAGE mode." - ) - } - - val startTime = SystemClock.uptimeMillis() - - val mpImage = BitmapImageBuilder(image).build() - - poseLandmarker?.detect(mpImage)?.also { landmarkResult -> - val inferenceTimeMs = SystemClock.uptimeMillis() - startTime - return ResultBundle( - listOf(landmarkResult), - inferenceTimeMs, - image.height, - image.width - ) - } - - poseLandmarkerHelperListener?.onError( - "Pose Landmarker failed to detect." - ) - return null - } - private fun returnLivestreamResult( result: PoseLandmarkerResult, input: MPImage diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/JourneyFragment.kt b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/JourneyFragment.kt index a7adcef860..dee950af39 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/JourneyFragment.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/JourneyFragment.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.fitquest.app.data.remote.RetrofitClient +import com.fitquest.app.data.remote.ServiceLocator import com.fitquest.app.databinding.FragmentJourneyBinding import com.fitquest.app.databinding.ItemScheduleBinding import com.fitquest.app.databinding.LayoutJourneyDaydetailBinding @@ -33,7 +34,7 @@ class JourneyFragment() : Fragment() { private val binding get() = _binding!! private val viewModel: JourneyViewModel by viewModels { - JourneyViewModelFactory(RetrofitClient.scheduleApiService) + JourneyViewModelFactory(ServiceLocator.scheduleApiService) } private lateinit var adapter: DailyWorkoutAdapter diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/ProfileFragment.kt b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/ProfileFragment.kt index d14b048386..d6bdf16415 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/ProfileFragment.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/ProfileFragment.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.fitquest.app.R import com.fitquest.app.data.remote.RetrofitClient +import com.fitquest.app.data.remote.ServiceLocator import com.fitquest.app.databinding.FragmentProfileBinding import com.fitquest.app.databinding.ItemExercisedoneBinding import com.fitquest.app.databinding.LayoutHistoryDetailBinding @@ -39,11 +40,12 @@ class ProfileFragment() : Fragment() { private var _binding: FragmentProfileBinding? = null private val binding get() = _binding!! private val historyViewModel: HistoryViewModel by viewModels { - HistoryViewModelFactory(RetrofitClient.dailySummaryApiService, RetrofitClient.scheduleApiService, RetrofitClient.sessionApiService) + HistoryViewModelFactory(ServiceLocator.dailySummaryApiService, ServiceLocator.scheduleApiService, + ServiceLocator.sessionApiService) } private val userViewModel: UserViewModel by viewModels { - UserViewModelFactory(RetrofitClient.userApiService) + UserViewModelFactory(ServiceLocator.userApiService) } private lateinit var historyAdapter: HistoryAdapter diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/LoginEmailFragment.kt b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/LoginEmailFragment.kt index 5f0d834500..b352058ce2 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/LoginEmailFragment.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/LoginEmailFragment.kt @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.fitquest.app.data.remote.RetrofitClient +import com.fitquest.app.data.remote.ServiceLocator import com.fitquest.app.databinding.FragmentLoginEmailBinding import com.fitquest.app.model.NetworkResult import com.fitquest.app.ui.viewmodels.AuthViewModel @@ -25,7 +26,7 @@ class LoginEmailFragment : Fragment() { private val binding get() = _binding!! private val viewModel: AuthViewModel by viewModels { - AuthViewModelFactory(RetrofitClient.authApiService, requireContext()) + AuthViewModelFactory(ServiceLocator.authApiService, requireContext()) } private lateinit var emailInput: TextInputEditText diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/LoginPasswordFragment.kt b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/LoginPasswordFragment.kt index 396c0ba641..bdad0abd66 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/LoginPasswordFragment.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/LoginPasswordFragment.kt @@ -12,6 +12,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.fitquest.app.MainActivity import com.fitquest.app.data.remote.RetrofitClient +import com.fitquest.app.data.remote.ServiceLocator import com.fitquest.app.data.remote.TokenManager import com.fitquest.app.databinding.FragmentLoginPasswordBinding import com.fitquest.app.model.NetworkResult @@ -25,7 +26,7 @@ class LoginPasswordFragment : Fragment() { private val binding get() = _binding!! private val authViewModel: AuthViewModel by viewModels { - AuthViewModelFactory(RetrofitClient.authApiService, requireContext()) + AuthViewModelFactory(ServiceLocator.authApiService, requireContext()) } private val args: LoginPasswordFragmentArgs by navArgs() diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupConstants.kt b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupConstants.kt index 7ed01a98d7..e26d190b84 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupConstants.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupConstants.kt @@ -5,17 +5,7 @@ object SignupConstants { const val COUNTDOWN_DURATION_SECONDS = 10 const val COUNTDOWN_INTERVAL_MS = 1000L - // Tracking lock FSM (CoachConstants와 동일) - const val VIS_THRESH = 0.95f - val LOWER_NEEDED = intArrayOf(23, 24, 25, 26, 27, 28) - const val LOWER_REQUIRED = 5 - const val BAD_VIS_LIMIT = 12 - const val GOOD_VIS_LIMIT = 12 - const val DISARM_MS_AFTER_UNLOCK = 300L - // Messages const val MSG_STEP_BACK = "STEP BACK" - // Logging (optional) - const val LOG_INTERVAL_MS = 0L // 0 = every frame } \ No newline at end of file diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupStep1Fragment.kt b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupStep1Fragment.kt index 6e76ba0d04..c313739715 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupStep1Fragment.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupStep1Fragment.kt @@ -17,6 +17,7 @@ import com.fitquest.app.ui.viewmodels.AuthViewModelFactory import kotlin.getValue import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.fitquest.app.data.remote.ServiceLocator import com.fitquest.app.util.animateLogo class SignupStep1Fragment : Fragment() { @@ -25,7 +26,7 @@ class SignupStep1Fragment : Fragment() { private val binding get() = _binding!! private val authViewModel: AuthViewModel by viewModels { - AuthViewModelFactory(RetrofitClient.authApiService, requireContext()) + AuthViewModelFactory(ServiceLocator.authApiService, requireContext()) } private val args: SignupStep1FragmentArgs by navArgs() diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupStep2Fragment.kt b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupStep2Fragment.kt index cdb052bf62..e0d88b11db 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupStep2Fragment.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/login/SignupStep2Fragment.kt @@ -31,6 +31,7 @@ import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import androidx.core.content.ContextCompat import com.fitquest.app.R +import com.fitquest.app.data.remote.ServiceLocator class SignupStep2Fragment : Fragment(), PoseLandmarkerHelper.LandmarkerListener { @@ -38,7 +39,7 @@ class SignupStep2Fragment : Fragment(), PoseLandmarkerHelper.LandmarkerListener private val binding get() = _binding!! private val authViewModel: AuthViewModel by viewModels { - AuthViewModelFactory(RetrofitClient.authApiService, requireContext()) + AuthViewModelFactory(ServiceLocator.authApiService, requireContext()) } private val args: SignupStep2FragmentArgs by navArgs() diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/pose/PoseFragment.kt b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/pose/PoseFragment.kt index 9aadaf85e0..faf0479c3d 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/pose/PoseFragment.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/ui/fragments/pose/PoseFragment.kt @@ -18,6 +18,7 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.fitquest.app.data.remote.RetrofitClient +import com.fitquest.app.data.remote.ServiceLocator import com.fitquest.app.databinding.FragmentPoseBinding import com.fitquest.app.model.NetworkResult import com.fitquest.app.model.pose.PoseUploadResponse @@ -37,7 +38,7 @@ class PoseFragment : Fragment() { private val binding get() = _binding!! private val viewModel: PoseViewModel by viewModels { - PoseViewModelFactory(RetrofitClient.poseAnalysisApiService) + PoseViewModelFactory(ServiceLocator.poseAnalysisApiService) } private lateinit var cameraManager: CameraManager diff --git a/android-studio-app/app/src/main/java/com/fitquest/app/ui/viewmodels/AiCoachViewModel.kt b/android-studio-app/app/src/main/java/com/fitquest/app/ui/viewmodels/AiCoachViewModel.kt index cf15ee5f9b..4471f0d158 100644 --- a/android-studio-app/app/src/main/java/com/fitquest/app/ui/viewmodels/AiCoachViewModel.kt +++ b/android-studio-app/app/src/main/java/com/fitquest/app/ui/viewmodels/AiCoachViewModel.kt @@ -29,16 +29,12 @@ class AiCoachViewModel( // ✅ 세션 준비 중 상태 추가 (카운트다운 포함) private val _sessionPreparing = MutableLiveData(false) - val sessionPreparing: LiveData = _sessionPreparing private val _currentSessionId = MutableLiveData(null) - val currentSessionId: LiveData = _currentSessionId private val _selectedExercise = MutableLiveData("") - val selectedExercise: LiveData = _selectedExercise private val _isTraining = MutableLiveData(false) - val isTraining: LiveData = _isTraining private val _repCount = MutableLiveData(0) val repCount: LiveData = _repCount @@ -47,17 +43,14 @@ class AiCoachViewModel( val points: LiveData = _points private val _feedback = MutableLiveData("") - val feedback: LiveData = _feedback // ✅ API 오류 메시지 전용 LiveData 추가 (Fragment에서 Toast 띄우기 위함) private val _errorMessage = MutableLiveData() val errorMessage: LiveData = _errorMessage - private val _formScore = MutableLiveData(0) - val formScore: LiveData = _formScore - private val _sessionActive = MutableLiveData(false) val sessionActive: LiveData = _sessionActive + private val _formScore = MutableLiveData(0) // ✅ 세션 준비 상태 설정 (카운트다운 시작) fun setSessionPreparing(preparing: Boolean) { @@ -192,18 +185,4 @@ class AiCoachViewModel( val currentMinPoseTrackingConfidence: Float get() = _minPoseTrackingConfidence val currentMinPosePresenceConfidence: Float get() = _minPosePresenceConfidence - fun setDelegate(delegate: Int) { _delegate = delegate } - fun setModel(model: Int) { _model = model } - - fun setMinPoseDetectionConfidence(v: Float) { - _minPoseDetectionConfidence = v - } - - fun setMinPoseTrackingConfidence(v: Float) { - _minPoseTrackingConfidence = v - } - - fun setMinPosePresenceConfidence(v: Float) { - _minPosePresenceConfidence = v - } } diff --git a/android-studio-app/app/src/main/res/layout/fragment_schedule.xml b/android-studio-app/app/src/main/res/layout/fragment_schedule.xml deleted file mode 100644 index 72f6a9e6b4..0000000000 --- a/android-studio-app/app/src/main/res/layout/fragment_schedule.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - -