Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8fa448b
test: kotest mockk turbine 설정과 초기 ViewModel BDD 테스트를 추가했어요
PeraSite Feb 14, 2026
a9d92cb
test: info/mypage 테스트를 보강하고 비동기 테스트 인프라를 안정화했어요
PeraSite Feb 14, 2026
7edc985
test: intro main map userinfo ViewModel BDD 테스트를 추가했어요
PeraSite Feb 14, 2026
1669cb6
test: 핵심 분기 UseCase BDD 테스트를 추가했어요
PeraSite Feb 14, 2026
f1f5235
test: ApiResult 유틸과 공통 계약 BDD 테스트를 추가했어요
PeraSite Feb 14, 2026
266b116
test: 네트워크 래퍼와 PagingSource BDD 테스트를 추가했어요
PeraSite Feb 14, 2026
4e32551
test: oauth와 user repository BDD 테스트를 추가했어요
PeraSite Feb 14, 2026
ed3ef1f
test: meal menu partnership repository BDD 테스트를 추가했어요
PeraSite Feb 14, 2026
bab627c
test: 나머지 remote repository BDD 테스트를 추가했어요
PeraSite Feb 14, 2026
fe48d51
test: 나머지 domain usecase BDD 테스트를 추가했어요
PeraSite Feb 14, 2026
b9580e6
test: DTO response mapper BDD 테스트를 추가했어요
PeraSite Feb 14, 2026
4bef13c
test: alarm usecase 스펙을 전체 스위트 기준으로 안정화했어요
PeraSite Feb 14, 2026
b66d890
feat: Clock의 의존성 주입을 추가했어요
PeraSite Feb 14, 2026
5302739
test: 네트워크 어댑터와 로그인 유틸 테스트를 추가했어요
PeraSite Feb 14, 2026
49dbc74
test: 누락 분기 테스트와 공용 DSL을 보강했어요
PeraSite Feb 14, 2026
9058c93
test: 유저 정보 저장 분기 조건 테스트를 보강했어요
PeraSite Feb 14, 2026
d82c9ec
fix: reflect gemini review feedback in review and user info flows
PeraSite Feb 14, 2026
3ad6314
fix: 고정 메뉴 리뷰에서 빈 likeMenuIdList를 null로 처리했어요
PeraSite Feb 14, 2026
2486cb6
fix: 고정 메뉴 리뷰에서 빈 likeMenuIdList를 null로 처리했어요
PeraSite Feb 14, 2026
cf6ac98
fix: 닉네임은 원격 변경 성공 후에만 로컬 저장하도록 수정했어요
PeraSite Feb 14, 2026
2ccd914
ci: debug workflow에서 unit test 통과를 필수화했어요
PeraSite Feb 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/debug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Run unit tests (required)
run: ./gradlew :app:testDebugUnitTest :core:common:testDebugUnitTest

# - name: Assemble Debug APK
# if: >
# github.event_name == 'pull_request' &&
Expand Down
14 changes: 13 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ android {
lint {
abortOnError = false
}

testOptions {
unitTests.all {
it.useJUnitPlatform()
}
}
}

dependencies {
Expand Down Expand Up @@ -180,6 +186,12 @@ dependencies {

// Testing libraries
testImplementation(libs.junit)
testImplementation(libs.kotest.runner.junit5)
testImplementation(libs.kotest.assertions.core)
testImplementation(libs.kotest.property)
testImplementation(libs.mockk)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.turbine)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.espresso.core)

Expand Down Expand Up @@ -269,4 +281,4 @@ dependencies {

configurations.all {
exclude(group = "io.github.fornewid", module = "naver-map-location")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ class ReviewRepositoryImpl @Inject constructor(private val reviewService: Review
rating = rating,
content = content,
imageUrls = imageUrls,
menuLike = likeMenuIdList?.let {
menuLike = likeMenuIdList?.firstOrNull()?.let {
WriteMenuReviewRequest.MenuLike(
menuId = it.first(),
menuId = it,
isLike = true,
)
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/eatssu/android/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton

@Module
Expand All @@ -17,4 +18,8 @@ object AppModule {
fun provideContext(application: Application): Context {
return application.applicationContext
}

@Provides
@Singleton
fun provideClock(): Clock = Clock.systemDefaultZone()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import android.content.Intent
import com.eatssu.android.alarm.NotificationReceiver
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.Calendar
import java.time.Clock
import javax.inject.Inject

class AlarmUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val clock: Clock,
) {

fun scheduleAlarm() {
Expand All @@ -21,13 +23,14 @@ class AlarmUseCase @Inject constructor(
)

val calendar = Calendar.getInstance().apply {
timeInMillis = clock.millis()
set(Calendar.HOUR_OF_DAY, 11)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}

if (calendar.timeInMillis <= System.currentTimeMillis()) {
if (calendar.timeInMillis <= clock.millis()) {
calendar.add(Calendar.DAY_OF_YEAR, 1)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ class SetUserNicknameUseCase @Inject constructor(
private val accountDataStore: AccountDataStore
) {
suspend operator fun invoke(nickname: String): Result<Unit> {
// 로컬 저장
accountDataStore.setName(nickname)
return userRepository.updateUserName(ChangeNicknameRequest(nickname))
val result = userRepository.updateUserName(ChangeNicknameRequest(nickname))
if (result.isSuccess) {
// 서버 닉네임 변경이 성공한 경우에만 로컬 닉네임 변경
accountDataStore.setName(nickname)
}
return result
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class ModifyViewModel @Inject constructor(
ToastType.ERROR
)
)
return@launch
}

_uiEvent.emit(UiEvent.NavigateBack)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ class WriteReviewViewModel @Inject constructor(
val compressedFile = compressImage(context, originalFile)
if (compressedFile != null && compressedFile.exists()) {
imageUrl = getImageUrlUseCase(compressedFile)
if (imageUrl == null) {
_uiState.value = UiState.Success(editing) // 되돌림
_uiEvent.emit(UiEvent.ShowToast(UiText.StringResource(R.string.toast_image_upload_failed), ToastType.ERROR))
originalFile.delete()
return@launch
}
_uiEvent.emit(UiEvent.ShowToast(UiText.StringResource(R.string.toast_image_upload_success), ToastType.SUCCESS))

// 원본 파일 삭제 (압축된 파일만 유지)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,30 @@ class UserInfoViewModel @Inject constructor(
fun saveUserInfo() {
viewModelScope.launch {
val currentState = _uiState.value as? UiState.Success ?: return@launch
val data = currentState.data

if ((data.isCollegeChanged || data.isDepartmentChanged) && data.selectedDepartment == null) {
_uiEvent.emit(
UiEvent.ShowToast(
UiText.StringResource(R.string.toast_department_required),
ToastType.ERROR
)
)
return@launch
}

if ((data.isCollegeChanged || data.isDepartmentChanged) && data.selectedCollege == null) {
_uiEvent.emit(
UiEvent.ShowToast(
UiText.StringResource(R.string.toast_college_required),
ToastType.ERROR
)
)
return@launch
}

_uiState.update { UiState.Loading }

val data = currentState.data
var nicknameUpdated = false
var departmentUpdated = false

Expand All @@ -223,8 +243,29 @@ class UserInfoViewModel @Inject constructor(

// 학과/단과대 변경이 있는 경우
if (data.isCollegeChanged || data.isDepartmentChanged) {
val department = data.selectedDepartment ?: return@launch
val college = data.selectedCollege ?: return@launch
val department = data.selectedDepartment
if (department == null) {
_uiState.value = UiState.Success(data)
_uiEvent.emit(
UiEvent.ShowToast(
UiText.StringResource(R.string.toast_department_required),
ToastType.ERROR
)
)
return@launch
}

val college = data.selectedCollege
if (college == null) {
_uiState.value = UiState.Success(data)
_uiEvent.emit(
UiEvent.ShowToast(
UiText.StringResource(R.string.toast_college_required),
ToastType.ERROR
)
)
return@launch
}

val success = userRepository.setUserDepartment(department.departmentId)
if (!success) {
Expand Down Expand Up @@ -305,4 +346,3 @@ data class UserInfoData(
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.eatssu.android.presentation.util

import java.text.SimpleDateFormat
import java.time.DayOfWeek
import java.time.Clock
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.Date
Expand Down Expand Up @@ -43,9 +44,9 @@ object CalendarUtil {
return formatter.format(date)
}

fun getNextDayDate(): String {
val nextDay = LocalDate.now().plusDays(1)
fun getNextDayDate(clock: Clock = Clock.systemDefaultZone()): String {
val nextDay = LocalDate.now(clock).plusDays(1)
val formatter = DateTimeFormatter.ofPattern("yyyyMMdd", Locale.getDefault())
return nextDay.format(formatter)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package com.eatssu.android.presentation.widget
import com.eatssu.android.domain.model.WidgetMealInfo
import com.eatssu.common.enums.Restaurant
import timber.log.Timber
import java.time.Clock
import java.time.LocalDateTime

/**
Expand All @@ -27,8 +28,12 @@ object WidgetCacheManager {
/**
* 캐시된 데이터가 유효한지 확인
*/
private fun isCacheValid(cachedData: CachedMealData, currentDate: String): Boolean {
val now = LocalDateTime.now()
private fun isCacheValid(
cachedData: CachedMealData,
currentDate: String,
clock: Clock,
): Boolean {
val now = LocalDateTime.now(clock)
val timeDiff = java.time.Duration.between(cachedData.timestamp, now)

return cachedData.date == currentDate &&
Expand All @@ -38,10 +43,14 @@ object WidgetCacheManager {
/**
* 캐시에서 식당별 메뉴 데이터 조회
*/
fun getCachedMealData(restaurant: Restaurant, currentDate: String): WidgetMealInfo? {
fun getCachedMealData(
restaurant: Restaurant,
currentDate: String,
clock: Clock = Clock.systemDefaultZone(),
): WidgetMealInfo? {
val cachedData = cacheMap[restaurant] ?: return null

return if (isCacheValid(cachedData, currentDate)) {
return if (isCacheValid(cachedData, currentDate, clock)) {
Timber.d("Cache hit for ${restaurant.name} on $currentDate")
cachedData.mealInfo
} else {
Expand All @@ -54,10 +63,15 @@ object WidgetCacheManager {
/**
* 식당별 메뉴 데이터를 캐시에 저장
*/
fun cacheMealData(restaurant: Restaurant, mealInfo: WidgetMealInfo, date: String) {
fun cacheMealData(
restaurant: Restaurant,
mealInfo: WidgetMealInfo,
date: String,
clock: Clock = Clock.systemDefaultZone(),
) {
val cachedData = CachedMealData(
mealInfo = mealInfo,
timestamp = LocalDateTime.now(),
timestamp = LocalDateTime.now(clock),
date = date
)

Expand Down Expand Up @@ -90,4 +104,4 @@ object WidgetCacheManager {
Timber.d("${restaurant.name}: ${data.date} at ${data.timestamp}")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.eatssu.android.presentation.util.CalendarUtil
import com.eatssu.android.presentation.widget.WidgetCacheManager
import com.eatssu.common.enums.Restaurant
import timber.log.Timber
import java.time.Clock
import java.time.LocalTime

sealed class MealTime {
Expand Down Expand Up @@ -37,12 +38,13 @@ object WidgetDataDisplayManager {
getMealsUseCase: GetTodayMealUseCase,
requestedMealTime: MealTime,
restaurant: Restaurant,
clock: Clock = Clock.systemDefaultZone(),
): WidgetMealInfo {
Timber.d("Widget - fetchMealInfo")
val targetDate = CalendarUtil.convertMillisToDateString(System.currentTimeMillis())
val targetDate = CalendarUtil.convertMillisToDateString(clock.millis())

// 캐시에서 데이터 확인
val cachedMealInfo = WidgetCacheManager.getCachedMealData(restaurant, targetDate)
val cachedMealInfo = WidgetCacheManager.getCachedMealData(restaurant, targetDate, clock)
if (cachedMealInfo != null) {
return cachedMealInfo
}
Expand All @@ -64,13 +66,13 @@ object WidgetDataDisplayManager {
)

// 캐시에 저장
WidgetCacheManager.cacheMealData(restaurant, mealInfo, targetDate)
WidgetCacheManager.cacheMealData(restaurant, mealInfo, targetDate, clock)

return mealInfo
}

// 다음 날 데이터 확인
val nextDay = CalendarUtil.getNextDayDate()
val nextDay = CalendarUtil.getNextDayDate(clock)
val getNextDayMealResponse = getMealsUseCase(nextDay, restaurant.name)

if (getNextDayMealResponse is MealState.Success) {
Expand All @@ -87,7 +89,7 @@ object WidgetDataDisplayManager {
)

// 캐시에 저장
WidgetCacheManager.cacheMealData(restaurant, mealInfo, targetDate)
WidgetCacheManager.cacheMealData(restaurant, mealInfo, targetDate, clock)

return mealInfo
}
Expand All @@ -101,13 +103,15 @@ object WidgetDataDisplayManager {
)

// 캐시에 저장
WidgetCacheManager.cacheMealData(restaurant, emptyMealInfo, targetDate)
WidgetCacheManager.cacheMealData(restaurant, emptyMealInfo, targetDate, clock)

return emptyMealInfo
}

internal fun getCurrentMealTime(): MealTime {
val currentTime = LocalTime.now()
internal fun getCurrentMealTime(
clock: Clock = Clock.systemDefaultZone(),
): MealTime {
val currentTime = LocalTime.now(clock)
val morningEnd = LocalTime.of(9, 0)
val lunchEnd = LocalTime.of(15, 0)

Expand All @@ -117,4 +121,4 @@ object WidgetDataDisplayManager {
else -> MealTime.Dinner
}
}
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
<!-- ========================== -->
<string name="toast_nickname_change_failed">닉네임 변경에 실패했어요.</string>
<string name="toast_college_required">단과대를 먼저 선택해주세요.</string>
<string name="toast_department_required">학과를 선택해주세요.</string>
<string name="toast_info_updated">정보가 업데이트되었습니다.</string>
<string name="toast_nickname_changed">닉네임이 변경되었습니다.</string>
<string name="toast_department_updated">학과 정보가 업데이트되었습니다.</string>
Expand Down
Loading