diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 749c9781e..45c4e842a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,13 +8,14 @@ plugins { alias(libs.plugins.hilt.android) id("kotlin-parcelize") id("kotlin-android") - id("kotlin-kapt") id("com.google.android.gms.oss-licenses-plugin") + id("kotlin-kapt") + } android { namespace = "com.eatssu.android" - compileSdk = 34 + compileSdk = 35 // S8: API 28 // S21: API 33 @@ -47,12 +48,11 @@ android { buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"$kakaoKey\"") manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = kakaoKey - isShrinkResources = false isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + + var shrinkResources = false + var minifyEnabled = false } debug { @@ -68,8 +68,6 @@ android { val kakaoKey: String = p.getProperty("KAKAO_NATIVE_APP_KEY") buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"$kakaoKey\"") manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = kakaoKey - - isMinifyEnabled = false } } @@ -78,14 +76,18 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" - } - composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } + kotlin { + jvmToolchain(17) + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + splits { abi { isEnable = true @@ -100,26 +102,34 @@ android { } dependencies { + implementation(project(":core:design-system")) + implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) - implementation(libs.constraintlayout) + implementation(libs.androidx.constraintlayout) implementation(libs.threetenabp) implementation(libs.material.calendarview) - implementation(libs.recyclerview) + implementation(libs.androidx.recyclerview) implementation(libs.transport.runtime) - implementation(libs.activity) - implementation(libs.fragment) - implementation(libs.androidx.activity) + implementation(libs.androidx.activity.ktx) + implementation(libs.androidx.fragment.ktx) + + //glance + implementation(libs.androidx.glance) + implementation(libs.androidx.glance.preview) + implementation(libs.androidx.glance.appwidget) + implementation(libs.androidx.glance.material3) + debugImplementation(libs.androidx.glance.appwidget.preview) // 프리뷰 지원 // Testing libraries testImplementation(libs.junit) androidTestImplementation(libs.androidx.test.ext.junit) - androidTestImplementation(libs.espresso.core) + androidTestImplementation(libs.androidx.espresso.core) //retrofit2: 서버통신 implementation(libs.retrofit) - implementation(libs.converter.gson) + implementation(libs.retrofit.converter.gson) // Gson for JSON parsing implementation(libs.gson) @@ -136,20 +146,23 @@ dependencies { implementation(libs.compressor) // Coroutines for concurrency - implementation(libs.coroutines) - implementation(libs.coroutines.core) - implementation(libs.lifecycle.runtime) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.androidx.lifecycle.runtime.ktx) // Kakao login SDK implementation(libs.kakao.login) // Hilt for Dependency Injection - implementation(libs.hilt) - kapt(libs.hilt.compiler) + implementation(libs.hilt.android) + kapt(libs.hilt.android.compiler) + kapt(libs.androidx.hilt.compiler) + implementation(libs.androidx.hilt.common) + implementation(libs.androidx.hilt.work) // ViewModel and LiveData - implementation(libs.lifecycle.viewmodel) - implementation(libs.lifecycle.livedata) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.play.services.base) @@ -167,26 +180,33 @@ dependencies { // Compose implementation(libs.androidx.activity.compose) - implementation(libs.androidx.animation) - implementation(libs.androidx.ui.tooling) - implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.androidx.lifecycle.runtime.ktx.v252) - implementation(libs.compose.bom) - implementation(libs.androidx.ui) - implementation(libs.androidx.ui.graphics) - implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.material3) - androidTestImplementation(libs.androidx.ui.test.junit4) + implementation(libs.androidx.compose.animation) + implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.lifecycle.viewmodel) + implementation(libs.androidx.compose.lifecycle.runtime) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) +// androidTestImplementation(libs.androidx.compose.ui.test.junit4) implementation(libs.compose.theme.adapter) implementation(libs.accompanist.appcompat.theme) - androidTestImplementation(libs.compose.bom) - debugImplementation(libs.androidx.ui.test.manifest) + androidTestImplementation(libs.androidx.compose.bom) + debugImplementation(libs.androidx.compose.ui.test.manifest) // navigation - implementation ("androidx.navigation:navigation-fragment:2.8.9") - implementation ("androidx.navigation:navigation-ui:2.8.9") + implementation(libs.androidx.navigation.fragment) + implementation(libs.androidx.navigation.ui) + + // worker (Kotlin + coroutines) + implementation(libs.androidx.work.runtime.ktx) + + //data store (with flow) + implementation(libs.androidx.datastore.preferences) + } kapt { correctErrorTypes = true -} +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 351fa65fa..ac8fba34e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,9 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -70,11 +107,23 @@ + - + + + + + + + + + + .mapTodayMenuResponseToMenu(): List { this.forEach { mealResponse -> val menuNames = - mealResponse.briefMenus.joinToString(separator = "+") { it.name ?: "" } + mealResponse.briefMenus.joinToString(separator = " + ") { it.name ?: "" } val mealId = mealResponse.mealId ?: -1 val price = mealResponse.price ?: 0 val mainRating = mealResponse.rating ?: 0.0 @@ -34,4 +34,11 @@ fun ArrayList.mapTodayMenuResponseToMenu(): List { } return menuList -} \ No newline at end of file +} + + +fun ArrayList.toDomain(): List> { + return this.map { meal -> + meal.briefMenus.mapNotNull { it.name } + } +} diff --git a/app/src/main/java/com/eatssu/android/data/enums/Restaurant.kt b/app/src/main/java/com/eatssu/android/data/enums/Restaurant.kt index 294a6bab1..693fcf065 100644 --- a/app/src/main/java/com/eatssu/android/data/enums/Restaurant.kt +++ b/app/src/main/java/com/eatssu/android/data/enums/Restaurant.kt @@ -1,5 +1,7 @@ package com.eatssu.android.data.enums + + enum class Restaurant(val displayName: String, val menuType: MenuType) { HAKSIK("학생 식당", MenuType.VARIABLE), DODAM("도담 식당", MenuType.VARIABLE), @@ -8,4 +10,18 @@ enum class Restaurant(val displayName: String, val menuType: MenuType) { FOOD_COURT("푸드 코트", MenuType.FIXED), SNACK_CORNER("스낵 코너", MenuType.FIXED), THE_KITCHEN("더 키친", MenuType.FIXED); + + companion object { + + fun getVariableRestaurantList(): List { + return entries.filter { it.menuType == MenuType.VARIABLE } + } + + fun fromRestaurantEnumName(enumName: String): String { + return entries.find { it.name == enumName }?.displayName ?: "" + } + + fun fromDisplayName(name: String): String = + entries.find { it.displayName == name }?.name ?: error("Unknown display name: $name") + } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/enums/Time.kt b/app/src/main/java/com/eatssu/android/data/enums/Time.kt index b94486f26..74f1dc8de 100644 --- a/app/src/main/java/com/eatssu/android/data/enums/Time.kt +++ b/app/src/main/java/com/eatssu/android/data/enums/Time.kt @@ -1,5 +1,13 @@ package com.eatssu.android.data.enums -enum class Time { - MORNING, LUNCH, DINNER +enum class Time(val displayName: String) { + MORNING("조식"), + LUNCH("중식"), + DINNER("석식"); + + companion object { + fun fromTimeEnumName(enumName: String): String { + return Time.values().find { it.name == enumName }?.displayName ?: "" + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/repository/MealRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/MealRepositoryImpl.kt index 38d861730..660e76e93 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/MealRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/MealRepositoryImpl.kt @@ -2,21 +2,40 @@ package com.eatssu.android.data.repository import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.data.dto.response.MenuOfMealResponse +import com.eatssu.android.data.dto.response.toDomain import com.eatssu.android.data.service.MealService import com.eatssu.android.domain.repository.MealRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import javax.inject.Inject -class MealRepositoryImpl @Inject constructor(private val mealService: MealService) : - MealRepository { +class MealRepositoryImpl @Inject constructor( + private val mealService: MealService, +) : MealRepository { + + override suspend fun getTodayMeal( //todo 분기처리 어떻게 할지? + date: String, + restaurant: String, + time: String + ): Flow>> { + return flow { + try { + val response = mealService.getTodayMeal2(date, restaurant, time) + + // 응답이 성공적이라면 Result.success()로 감싸서 Flow로 반환 + if (response.isSuccess == true) { + response.result?.let { emit(it.toDomain()) } // 성공시 데이터를 반환 + } else { + // 실패한 경우에는 Result.failure()로 실패 정보 반환 + emit(emptyList()) + } + } catch (e: Exception) { + // 네트워크 오류 또는 예외가 발생한 경우에는 Result.failure()로 반환 +// emit(ApiResult.Failure(e)) + } + } + } -// override suspend fun getTodayMeal( -// date: String, -// restaurant: String, -// time: String -// ): Flow>> = -// flow { emit(mealService.getTodayMeal(date, restaurant, time)) } override suspend fun getMenuInfoByMealId(mealId: Long): Flow> = flow { diff --git a/app/src/main/java/com/eatssu/android/data/repository/WidgetPreferencesRepository.kt b/app/src/main/java/com/eatssu/android/data/repository/WidgetPreferencesRepository.kt new file mode 100644 index 000000000..a6fe114c7 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/data/repository/WidgetPreferencesRepository.kt @@ -0,0 +1,37 @@ +package com.eatssu.android.data.repository + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.eatssu.android.data.enums.Restaurant +import kotlinx.coroutines.flow.first +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class WidgetPreferencesRepository @Inject constructor( + private val context: Context, +) { + private val Context.widgetPrefsDataStore: DataStore by preferencesDataStore(name = "widget_prefs") + private fun fileKeyRestaurantKey(fileKey: String) = + stringPreferencesKey("widget_restaurant_by_fileKey_$fileKey") + + suspend fun saveRestaurantByFileKey(fileKey: String, restaurant: String) { + context.widgetPrefsDataStore.edit { prefs -> + prefs[fileKeyRestaurantKey(fileKey)] = restaurant + } + Timber.d("saveRestaurantByFileKey 호출됨: fileKey='$fileKey', restaurant='$restaurant'") + } + + suspend fun loadRestaurantByFileKey(fileKey: String): Restaurant? { + val prefs: Preferences = context.widgetPrefsDataStore.data.first() + val value = prefs[fileKeyRestaurantKey(fileKey)] + Timber.d("loadRestaurantByFileKey 호출됨: fileKey='$fileKey', value='$value'") + if (value.isNullOrBlank()) return null + return runCatching { Restaurant.valueOf(value) }.getOrNull() + } +} diff --git a/app/src/main/java/com/eatssu/android/data/service/MealService.kt b/app/src/main/java/com/eatssu/android/data/service/MealService.kt index af604ecd6..fabe0a657 100644 --- a/app/src/main/java/com/eatssu/android/data/service/MealService.kt +++ b/app/src/main/java/com/eatssu/android/data/service/MealService.kt @@ -19,6 +19,14 @@ interface MealService { @Query("time") time: String, ): Call>> + // todo 위에 함수를 call 없애서 하나로 합치길 바람 ㅜㅜ 위젯 때문에 급하게 복사본을 만듦 + @GET("meals") + suspend fun getTodayMeal2( + @Query("date") date: String, + @Query("restaurant") restaurant: String, + @Query("time") time: String, + ): BaseResponse> + /** * 메뉴 정보 리스트 조회 */ diff --git a/app/src/main/java/com/eatssu/android/di/AppModule.kt b/app/src/main/java/com/eatssu/android/di/AppModule.kt index 93cc62b8a..055b6f18d 100644 --- a/app/src/main/java/com/eatssu/android/di/AppModule.kt +++ b/app/src/main/java/com/eatssu/android/di/AppModule.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.Context import com.eatssu.android.data.repository.FirebaseRemoteConfigRepository import com.eatssu.android.data.repository.PreferencesRepository +import com.eatssu.android.data.repository.WidgetPreferencesRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -32,4 +33,10 @@ object AppModule { fun provideFirebaseRemoteConfigRepository(): FirebaseRemoteConfigRepository { return FirebaseRemoteConfigRepository() } + + @Provides + @Singleton + fun provideWidgetPreferencesRepository(@ApplicationContext context: Context): WidgetPreferencesRepository { + return WidgetPreferencesRepository(context) + } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/di/DataModule.kt b/app/src/main/java/com/eatssu/android/di/DataModule.kt index cceec61ad..0e65f15cc 100644 --- a/app/src/main/java/com/eatssu/android/di/DataModule.kt +++ b/app/src/main/java/com/eatssu/android/di/DataModule.kt @@ -1,15 +1,15 @@ package com.eatssu.android.di -import com.eatssu.android.domain.repository.MealRepository import com.eatssu.android.data.repository.MealRepositoryImpl -import com.eatssu.android.domain.repository.OauthRepository import com.eatssu.android.data.repository.OauthRepositoryImpl -import com.eatssu.android.domain.repository.ReportRepository import com.eatssu.android.data.repository.ReportRepositoryImpl +import com.eatssu.android.data.repository.UserRepositoryImpl +import com.eatssu.android.domain.repository.MealRepository +import com.eatssu.android.domain.repository.OauthRepository +import com.eatssu.android.domain.repository.ReportRepository import com.eatssu.android.domain.repository.ReviewRepository import com.eatssu.android.domain.repository.UserRepository -import com.eatssu.android.data.repository.UserRepositoryImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/eatssu/android/di/NetworkModule.kt b/app/src/main/java/com/eatssu/android/di/NetworkModule.kt index 8c4b9fed9..683a1b5cd 100644 --- a/app/src/main/java/com/eatssu/android/di/NetworkModule.kt +++ b/app/src/main/java/com/eatssu/android/di/NetworkModule.kt @@ -3,15 +3,11 @@ package com.eatssu.android.di import com.eatssu.android.BuildConfig import com.eatssu.android.BuildConfig.BASE_URL -import com.eatssu.android.data.service.MealService -import com.eatssu.android.data.service.MenuService -import com.eatssu.android.data.service.OauthService -import com.eatssu.android.data.service.ReportService -import com.eatssu.android.data.service.ReviewService -import com.eatssu.android.data.service.UserService import com.eatssu.android.di.network.TokenAuthenticator import com.eatssu.android.di.network.TokenInterceptor +import com.eatssu.android.domain.usecase.auth.GetRefreshTokenUseCase import com.eatssu.android.domain.usecase.auth.LogoutUseCase +import com.eatssu.android.domain.usecase.auth.ReissueTokenUseCase import com.eatssu.android.domain.usecase.auth.SetAccessTokenUseCase import com.eatssu.android.domain.usecase.auth.SetRefreshTokenUseCase import dagger.Module @@ -25,10 +21,8 @@ import retrofit2.Converter import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.lang.reflect.Type -import javax.inject.Singleton -import com.eatssu.android.domain.usecase.auth.GetRefreshTokenUseCase -import com.eatssu.android.domain.usecase.auth.ReissueTokenUseCase import javax.inject.Qualifier +import javax.inject.Singleton class NullOnEmptyConverterFactory : Converter.Factory() { override fun responseBodyConverter( @@ -131,11 +125,4 @@ object NetworkModule { logoutUseCase, ) } - - // provide service - @Provides - @Singleton - fun provideOauthService(@NoToken noTokenRetrofit: Retrofit): OauthService { - return noTokenRetrofit.create(OauthService::class.java) - } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/di/ServiceModule.kt b/app/src/main/java/com/eatssu/android/di/ServiceModule.kt index bf13699e3..a584f008d 100644 --- a/app/src/main/java/com/eatssu/android/di/ServiceModule.kt +++ b/app/src/main/java/com/eatssu/android/di/ServiceModule.kt @@ -2,6 +2,7 @@ package com.eatssu.android.di import com.eatssu.android.data.service.MealService import com.eatssu.android.data.service.MenuService +import com.eatssu.android.data.service.OauthService import com.eatssu.android.data.service.ReportService import com.eatssu.android.data.service.ReviewService import com.eatssu.android.data.service.UserService @@ -15,7 +16,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object ServiceModule { - + @Provides + @Singleton + fun provideOauthService(@NoToken noTokenRetrofit: Retrofit): OauthService { + return noTokenRetrofit.create(OauthService::class.java) + } @Provides @Singleton fun provideUserService(retrofit: Retrofit): UserService { @@ -36,8 +41,8 @@ object ServiceModule { @Provides @Singleton - fun provideMealService(retrofit: Retrofit): MealService { - return retrofit.create(MealService::class.java) + fun provideMealService(@NoToken noTokenRetrofit: Retrofit): MealService { + return noTokenRetrofit.create(MealService::class.java) } @Provides diff --git a/app/src/main/java/com/eatssu/android/di/TaskModule.kt b/app/src/main/java/com/eatssu/android/di/TaskModule.kt new file mode 100644 index 000000000..f1dceed36 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/di/TaskModule.kt @@ -0,0 +1,21 @@ +package com.eatssu.android.di + +import android.content.Context +import androidx.work.WorkManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object TaskModule { + + @Singleton + @Provides + fun provideWorkManager(@ApplicationContext context: Context): WorkManager { + return WorkManager.getInstance(context) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/di/network/TokenAuthenticator.kt b/app/src/main/java/com/eatssu/android/di/network/TokenAuthenticator.kt index 6a9b23625..38d7db194 100644 --- a/app/src/main/java/com/eatssu/android/di/network/TokenAuthenticator.kt +++ b/app/src/main/java/com/eatssu/android/di/network/TokenAuthenticator.kt @@ -2,8 +2,12 @@ package com.eatssu.android.di.network import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.data.dto.response.TokenResponse -import com.eatssu.android.domain.usecase.auth.* import com.eatssu.android.domain.model.TokenStateManager +import com.eatssu.android.domain.usecase.auth.GetRefreshTokenUseCase +import com.eatssu.android.domain.usecase.auth.LogoutUseCase +import com.eatssu.android.domain.usecase.auth.ReissueTokenUseCase +import com.eatssu.android.domain.usecase.auth.SetAccessTokenUseCase +import com.eatssu.android.domain.usecase.auth.SetRefreshTokenUseCase import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.runBlocking import okhttp3.Authenticator @@ -27,7 +31,7 @@ class TokenAuthenticator @Inject constructor( ) : Authenticator { /** - * 401 Unauthorized 응답을h 받았을 때 호출되는 메서드 + * 401 Unauthorized 응답을 받았을 때 호출되는 메서드 * @param route : 요청한 경로 * @param response : 응답 객체 * @return : 새로운 요청 객체 diff --git a/app/src/main/java/com/eatssu/android/domain/model/WidgetMealInfo.kt b/app/src/main/java/com/eatssu/android/domain/model/WidgetMealInfo.kt new file mode 100644 index 000000000..8eb78c9cf --- /dev/null +++ b/app/src/main/java/com/eatssu/android/domain/model/WidgetMealInfo.kt @@ -0,0 +1,17 @@ +package com.eatssu.android.domain.model + +import com.eatssu.android.data.enums.Restaurant + + +sealed interface WidgetMealInfo { + object Loading : WidgetMealInfo + + data class Available( + val breakfast: List>, + val lunch: List>, + val dinner: List>, + val restaurant: Restaurant, + ) : WidgetMealInfo + + object Unavailable : WidgetMealInfo +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/repository/MealRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/MealRepository.kt index c6ff41846..1aac5a02d 100644 --- a/app/src/main/java/com/eatssu/android/domain/repository/MealRepository.kt +++ b/app/src/main/java/com/eatssu/android/domain/repository/MealRepository.kt @@ -5,12 +5,20 @@ import com.eatssu.android.data.dto.response.MenuOfMealResponse import kotlinx.coroutines.flow.Flow interface MealRepository { -// suspend fun getTodayMeal( -// date: String, -// restaurant: String, -// time: String, -// ): Flow>> + /** + * 오늘의 식단을 가져오는 api + */ + suspend fun getTodayMeal( + date: String, + restaurant: String, + time: String, + ): Flow>> + + + /** + * MealId를 이용해서 Menu를 찾기 api + */ suspend fun getMenuInfoByMealId( mealId: Long, ): Flow> diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/widget/GetTodayMealUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/widget/GetTodayMealUseCase.kt new file mode 100644 index 000000000..bb8d0d28f --- /dev/null +++ b/app/src/main/java/com/eatssu/android/domain/usecase/widget/GetTodayMealUseCase.kt @@ -0,0 +1,75 @@ +package com.eatssu.android.domain.usecase.widget + +import com.eatssu.android.data.enums.Restaurant +import com.eatssu.android.data.enums.Time +import com.eatssu.android.domain.repository.MealRepository +import com.eatssu.android.presentation.widget.WidgetMealList +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import timber.log.Timber +import java.net.UnknownHostException +import java.nio.channels.UnresolvedAddressException +import javax.inject.Inject + +sealed interface MealException { + /** 급식 정보가 없음 */ + data object DataEmpty : MealException + + /** 인터넷 연결 X */ + data object InternetDisconnected : MealException + + /** + * 알 수 없는 에러 with errorCode + * - 사용자가 알 필요가 없는 remote error + * - 코드상 문제로 인한 exception + * */ + data class Unknown(val errorCode: String) : MealException +} + +sealed interface MealState { + data object Loading : MealState + + data class Success(val response: WidgetMealList) : MealState + + data object Failure : MealState +} + + +class GetTodayMealUseCase @Inject constructor( + private val mealRepository: MealRepository, +) { + suspend operator fun invoke( + date: String, + restaurant: String + ): MealState = runCatching { + val breakfastFlow = mealRepository.getTodayMeal(date, restaurant, Time.MORNING.name) + val lunchFlow = mealRepository.getTodayMeal(date, restaurant, Time.LUNCH.name) + val dinnerFlow = mealRepository.getTodayMeal(date, restaurant, Time.DINNER.name) + + combine(breakfastFlow, lunchFlow, dinnerFlow) { breakfastList, lunchList, dinnerList -> + + WidgetMealList( + breakfast = (breakfastList to "breakfast"), + lunch = (lunchList to "lunch"), + dinner = (dinnerList to "dinner"), + restaurant = Restaurant.valueOf(restaurant) + ) + }.first() // 여기서 Flow 실행 + }.fold( + onSuccess = { result -> + Timber.d("메뉴 가져오기 성공 $result") + MealState.Success(result) + }, + onFailure = { exception -> + val error = when (exception) { + + is UnresolvedAddressException, is UnknownHostException -> MealException.InternetDisconnected + + else -> MealException.Unknown("알 수 없는 에러") + } + + MealState.Failure + } + ) +} + diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/widget/LoadRestaurantByFileKeyUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/widget/LoadRestaurantByFileKeyUseCase.kt new file mode 100644 index 000000000..9f7169fa6 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/domain/usecase/widget/LoadRestaurantByFileKeyUseCase.kt @@ -0,0 +1,13 @@ +package com.eatssu.android.domain.usecase.widget + +import com.eatssu.android.data.enums.Restaurant +import com.eatssu.android.data.repository.WidgetPreferencesRepository +import javax.inject.Inject + +class LoadRestaurantByFileKeyUseCase @Inject constructor( + private val widgetPrefsRepository: WidgetPreferencesRepository +) { + suspend operator fun invoke(fileKey: String): Restaurant? { + return widgetPrefsRepository.loadRestaurantByFileKey(fileKey) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/widget/SaveRestaurantByFileKeyUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/widget/SaveRestaurantByFileKeyUseCase.kt new file mode 100644 index 000000000..fe78e8c89 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/domain/usecase/widget/SaveRestaurantByFileKeyUseCase.kt @@ -0,0 +1,12 @@ +package com.eatssu.android.domain.usecase.widget + +import com.eatssu.android.data.repository.WidgetPreferencesRepository +import javax.inject.Inject + +class SaveRestaurantByFileKeyUseCase @Inject constructor( + private val widgetPrefsRepository: WidgetPreferencesRepository +) { + suspend operator fun invoke(fileKey: String, restaurantDisplayName: String) { + widgetPrefsRepository.saveRestaurantByFileKey(fileKey, restaurantDisplayName) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt b/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt index f379eb802..8a613e4a1 100644 --- a/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt @@ -14,6 +14,7 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.NavHostFragment +import androidx.work.WorkManager import com.eatssu.android.R import com.eatssu.android.databinding.ActivityMainBinding import com.eatssu.android.presentation.base.BaseActivity @@ -28,10 +29,14 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.Locale +import javax.inject.Inject -@AndroidEntryPoint -class MainActivity : BaseActivity(ActivityMainBinding::inflate) { +@AndroidEntryPoint +class MainActivity : BaseActivity(ActivityMainBinding::inflate){ + @Inject + lateinit var workManager: WorkManager + private val mainViewModel: MainViewModel by viewModels() private val myPageViewModel: MyPageViewModel by viewModels() diff --git a/app/src/main/java/com/eatssu/android/presentation/map/MapFragment.kt b/app/src/main/java/com/eatssu/android/presentation/map/MapFragment.kt index 591a1d6a2..29963fa64 100644 --- a/app/src/main/java/com/eatssu/android/presentation/map/MapFragment.kt +++ b/app/src/main/java/com/eatssu/android/presentation/map/MapFragment.kt @@ -1,12 +1,12 @@ package com.eatssu.android.presentation.map import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView -import com.eatssu.android.presentation.compose.ui.theme.EatssuTheme +import androidx.fragment.app.Fragment +import com.eatssu.design_system.theme.EatssuTheme class MapFragment : Fragment() { diff --git a/app/src/main/java/com/eatssu/android/presentation/map/MapFragmentComposeView.kt b/app/src/main/java/com/eatssu/android/presentation/map/MapFragmentComposeView.kt index 43d138691..2e06293b0 100644 --- a/app/src/main/java/com/eatssu/android/presentation/map/MapFragmentComposeView.kt +++ b/app/src/main/java/com/eatssu/android/presentation/map/MapFragmentComposeView.kt @@ -3,8 +3,8 @@ package com.eatssu.android.presentation.map import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import com.eatssu.android.presentation.compose.ui.theme.EatssuTheme -import com.eatssu.android.presentation.compose.ui.theme.Primary +import com.eatssu.design_system.theme.EatssuTheme +import com.eatssu.design_system.theme.Primary @Composable fun MapFragmentComposeView() { diff --git a/app/src/main/java/com/eatssu/android/presentation/util/CalendarUtil.kt b/app/src/main/java/com/eatssu/android/presentation/util/CalendarUtil.kt index b1215ba68..cd95c0ac8 100644 --- a/app/src/main/java/com/eatssu/android/presentation/util/CalendarUtil.kt +++ b/app/src/main/java/com/eatssu/android/presentation/util/CalendarUtil.kt @@ -2,9 +2,13 @@ package com.eatssu.android.presentation.util import android.os.Build import androidx.annotation.RequiresApi +import java.text.SimpleDateFormat import java.time.DayOfWeek import java.time.LocalDate import java.time.format.DateTimeFormatter +import java.util.Calendar +import java.util.Date +import java.util.Locale object CalendarUtil { @@ -38,4 +42,23 @@ object CalendarUtil { } return null } + + fun convertMillisToDateString(millis: Long): String { + val formatter = SimpleDateFormat("yyyyMMdd", Locale.getDefault()) + val date = Date(millis) + return formatter.format(date) + } + + fun getNextDayDate(): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val nextDay = LocalDate.now().plusDays(1) + val formatter = DateTimeFormatter.ofPattern("yyyyMMdd", Locale.getDefault()) + nextDay.format(formatter) + } else { + val calendar = Calendar.getInstance() + calendar.add(Calendar.DAY_OF_MONTH, 1) + val formatter = SimpleDateFormat("yyyyMMdd", Locale.getDefault()) + formatter.format(calendar.time) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/util/TimeUtil.kt.kt b/app/src/main/java/com/eatssu/android/presentation/util/TimeUtil.kt.kt new file mode 100644 index 000000000..5e0ca3d4d --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/util/TimeUtil.kt.kt @@ -0,0 +1,12 @@ +package com.eatssu.android.presentation.util + +object TimeUtil { + fun getTimeIndex(time: Int): Int { + return when (time) { + in 0..9 -> 0 //아침 + in 10..15 -> 1 //점심 + in 16..24 -> 2 //저녁 + else -> 3 + } + } +} diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/MealInfoStateDefinition.kt b/app/src/main/java/com/eatssu/android/presentation/widget/MealInfoStateDefinition.kt new file mode 100644 index 000000000..eb1a3d00b --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/MealInfoStateDefinition.kt @@ -0,0 +1,110 @@ +package com.eatssu.android.presentation.widget + +import android.content.Context +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.Serializer +import androidx.datastore.dataStoreFile +import androidx.glance.state.GlanceStateDefinition +import com.eatssu.android.data.enums.Restaurant +import com.eatssu.android.domain.model.WidgetMealInfo +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import timber.log.Timber +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import java.util.concurrent.ConcurrentHashMap + + +object MealInfoStateDefinition : GlanceStateDefinition { + + private const val DATA_STORE_FILENAME_PREFIX = "MealInfo_" + + private val dataStoreScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val storeByPath = ConcurrentHashMap>() + + override suspend fun getDataStore( + context: Context, + fileKey: String + ): DataStore { + val file = getLocation(context, fileKey) + val path = file.absolutePath + return storeByPath.getOrPut(path) { + Timber.d("Create DataStore for $path") + DataStoreFactory.create( + serializer = MealInfoSerializer, + produceFile = { file }, + scope = dataStoreScope, + ) + } + } + + override fun getLocation(context: Context, fileKey: String): File { + // fileKey를 그대로 사용하여 파일명 생성 (appWidget-25 -> MealInfo_appWidget-25) + val filename = "${DATA_STORE_FILENAME_PREFIX}${fileKey}" + val file = context.dataStoreFile(filename) + Timber.d("Glance location by widget '$fileKey' -> ${file.absolutePath}") + return file + } + + object MealInfoSerializer : Serializer { + private val gson = Gson() + override val defaultValue = WidgetMealInfo.Loading + + override suspend fun readFrom(input: InputStream): WidgetMealInfo { + return try { + val jsonRaw = input.readBytes().decodeToString() + Timber.d("readFrom: raw = '$jsonRaw'") + if (jsonRaw.isBlank()) return defaultValue + val obj = JsonParser.parseString(jsonRaw).asJsonObject + when (obj.get("type").asString) { + "Loading" -> WidgetMealInfo.Loading + "Unavailable" -> WidgetMealInfo.Unavailable + "Available" -> { + val mealListType = object : TypeToken>>() {}.type + val breakfast = + gson.fromJson>>(obj.get("breakfast"), mealListType) + val lunch = + gson.fromJson>>(obj.get("lunch"), mealListType) + val dinner = + gson.fromJson>>(obj.get("dinner"), mealListType) + val restaurant = Restaurant.valueOf(obj.get("restaurant").asString) + WidgetMealInfo.Available(breakfast, lunch, dinner, restaurant) + } + + else -> defaultValue + } + } catch (e: Exception) { + Timber.e("Serialization error: ${e.message}") + throw CorruptionException("Could not read data: ${e.message}") + } + } + + override suspend fun writeTo(t: WidgetMealInfo, output: OutputStream) { + val obj = JsonObject() + when (t) { + is WidgetMealInfo.Loading -> obj.addProperty("type", "Loading") + is WidgetMealInfo.Unavailable -> obj.addProperty("type", "Unavailable") + is WidgetMealInfo.Available -> { + obj.addProperty("type", "Available") + obj.add("breakfast", gson.toJsonTree(t.breakfast)) + obj.add("lunch", gson.toJsonTree(t.lunch)) + obj.add("dinner", gson.toJsonTree(t.dinner)) + obj.addProperty("restaurant", t.restaurant.name) + } + } + val json = gson.toJson(obj) + Timber.d("[writeTo] json = $json") + output.use { + it.write(json.encodeToByteArray()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/MealWidgetReceiver.kt b/app/src/main/java/com/eatssu/android/presentation/widget/MealWidgetReceiver.kt new file mode 100644 index 000000000..de8939a6e --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/MealWidgetReceiver.kt @@ -0,0 +1,39 @@ +package com.eatssu.android.presentation.widget + +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import com.eatssu.android.presentation.widget.ui.MealWidget +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import java.io.File + +class MealWidgetReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget + get() = MealWidget() + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + super.onDeleted(context, appWidgetIds) + + // 삭제된 위젯들의 DataStore 파일 정리 + appWidgetIds.forEach { appWidgetId -> + cleanupWidgetDataStore(context, appWidgetId) + } + } + + private fun cleanupWidgetDataStore(context: Context, appWidgetId: Int) { + try { + runBlocking { + val filename = "appWidgetLayout-${appWidgetId}" + val dataStoreFile = File(context.filesDir, "datastore/$filename") + + if (dataStoreFile.exists()) { + dataStoreFile.delete() + Timber.d("Deleted DataStore file for widget $appWidgetId") + } + } + } catch (e: Exception) { + Timber.e("Failed to cleanup DataStore for widget $appWidgetId: ${e.message}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/MealWorker.kt b/app/src/main/java/com/eatssu/android/presentation/widget/MealWorker.kt new file mode 100644 index 000000000..005c33af3 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/MealWorker.kt @@ -0,0 +1,91 @@ +package com.eatssu.android.presentation.widget + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.state.updateAppWidgetState +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.eatssu.android.domain.model.WidgetMealInfo +import com.eatssu.android.domain.usecase.widget.GetTodayMealUseCase +import com.eatssu.android.domain.usecase.widget.LoadRestaurantByFileKeyUseCase +import com.eatssu.android.presentation.widget.ui.MealWidget +import com.eatssu.android.presentation.widget.util.WidgetDataDisplayManager +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import timber.log.Timber +import java.time.Duration + +@HiltWorker +class MealWorker @AssistedInject constructor( + @Assisted private val context: Context, + @Assisted workParams: WorkerParameters, + private var getMealsUseCase: GetTodayMealUseCase, + private var loadRestaurantByFileKeyUseCase: LoadRestaurantByFileKeyUseCase, +) : CoroutineWorker(context, workParams) { + companion object { + private val uniqueWorkName = MealWorker::class.java.simpleName + + @RequiresApi(Build.VERSION_CODES.O) + fun enqueue(context: Context) { + val manager = WorkManager.getInstance(context) + val requestBuilder = PeriodicWorkRequestBuilder( + Duration.ofMinutes(60) + ) + + Timber.d("Widget - enqueue") + manager.enqueueUniquePeriodicWork( + uniqueWorkName, + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + requestBuilder.build() + ) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + override suspend fun doWork(): Result { + val manager = GlanceAppWidgetManager(context) + val glanceIds = manager.getGlanceIds(MealWidget::class.java) + glanceIds.forEach { glanceId -> + val appWidgetId = manager.getAppWidgetId(glanceId) + // glanceId를 사용하여 정확한 식당 정보 가져오기 + val restaurant = loadRestaurantByFileKeyUseCase("appWidget-${appWidgetId}") + Timber.d("MealWorker: glanceId=$glanceId, appWidgetId=$appWidgetId, restaurant=$restaurant") + if (restaurant != null) { + // 저장된 식당 정보가 있으면 3개 식사 시간의 메뉴를 모두 가져와서 위젯 상태 업데이트 + val currentMealTime = WidgetDataDisplayManager.getCurrentMealTime() + val newState = WidgetDataDisplayManager.fetchMealInfo( + getMealsUseCase = getMealsUseCase, + requestedMealTime = currentMealTime, + restaurant = restaurant + ) + + setWidgetState(glanceId = glanceId, newState = newState) + Timber.d("MealWorker: 위젯 상태 업데이트 완료 - 식당: ${restaurant.name}, 시간: $currentMealTime") + } else { + Timber.w("No restaurant saved for glanceId: $glanceId, skipping widget update") + } + } + + // 캐시 상태 로그 출력 + WidgetCacheManager.logCacheStatus() + Timber.d("Widget - 워커는 doWork") + return Result.success() + } + + private suspend fun setWidgetState(glanceId: GlanceId, newState: WidgetMealInfo) { + updateAppWidgetState( + context = context, + definition = MealInfoStateDefinition, + glanceId = glanceId, + updateState = { newState } + ) + MealWidget().update(context, glanceId) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/WidgetCacheManager.kt b/app/src/main/java/com/eatssu/android/presentation/widget/WidgetCacheManager.kt new file mode 100644 index 000000000..9f0f60742 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/WidgetCacheManager.kt @@ -0,0 +1,98 @@ +package com.eatssu.android.presentation.widget + + +import android.os.Build +import androidx.annotation.RequiresApi +import com.eatssu.android.data.enums.Restaurant +import com.eatssu.android.domain.model.WidgetMealInfo +import timber.log.Timber +import java.time.LocalDateTime + +/** + * 식당별 위젯 데이터 캐싱을 관리하는 클래스 + * 각 식당의 메뉴 데이터를 캐싱하여 중복 API 호출을 방지합니다. + */ +object WidgetCacheManager { + + // 캐시 유효 시간 (분) + private const val CACHE_VALIDITY_MINUTES = 30L + + // 식당별 캐시 데이터 + private val cacheMap = mutableMapOf() + + data class CachedMealData( + val mealInfo: WidgetMealInfo, + val timestamp: LocalDateTime, + val date: String + ) + + /** + * 캐시된 데이터가 유효한지 확인 + */ + @RequiresApi(Build.VERSION_CODES.O) + private fun isCacheValid(cachedData: CachedMealData, currentDate: String): Boolean { + val now = LocalDateTime.now() + val timeDiff = java.time.Duration.between(cachedData.timestamp, now) + + return cachedData.date == currentDate && + timeDiff.toMinutes() < CACHE_VALIDITY_MINUTES + } + + /** + * 캐시에서 식당별 메뉴 데이터 조회 + */ + @RequiresApi(Build.VERSION_CODES.O) + fun getCachedMealData(restaurant: Restaurant, currentDate: String): WidgetMealInfo? { + val cachedData = cacheMap[restaurant] ?: return null + + return if (isCacheValid(cachedData, currentDate)) { + Timber.d("Cache hit for ${restaurant.name} on $currentDate") + cachedData.mealInfo + } else { + Timber.d("Cache expired for ${restaurant.name} on $currentDate") + cacheMap.remove(restaurant) + null + } + } + + /** + * 식당별 메뉴 데이터를 캐시에 저장 + */ + @RequiresApi(Build.VERSION_CODES.O) + fun cacheMealData(restaurant: Restaurant, mealInfo: WidgetMealInfo, date: String) { + val cachedData = CachedMealData( + mealInfo = mealInfo, + timestamp = LocalDateTime.now(), + date = date + ) + + cacheMap[restaurant] = cachedData + Timber.d("Cached meal data for ${restaurant.name} on $date") + } + + /** + * 특정 식당의 캐시 데이터 삭제 + */ + fun clearCacheForRestaurant(restaurant: Restaurant) { + cacheMap.remove(restaurant) + Timber.d("Cleared cache for ${restaurant.name}") + } + + /** + * 모든 캐시 데이터 삭제 + */ + fun clearAllCache() { + cacheMap.clear() + Timber.d("Cleared all cache") + } + + /** + * 캐시 상태 로그 출력 + */ + fun logCacheStatus() { + Timber.d("Cache status: ${cacheMap.size} restaurants cached") + cacheMap.forEach { (restaurant, data) -> + Timber.d("${restaurant.name}: ${data.date} at ${data.timestamp}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/WidgetMealList.kt b/app/src/main/java/com/eatssu/android/presentation/widget/WidgetMealList.kt new file mode 100644 index 000000000..8ae14206c --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/WidgetMealList.kt @@ -0,0 +1,12 @@ +package com.eatssu.android.presentation.widget + +import com.eatssu.android.data.enums.Restaurant + +// 각 식사별로 여러 메뉴 그룹을 지원하도록 구조 변경 +// 예: lunch = ([ ["돈목살김치찜", "단호박카레볶음"], ["간짜장덮밥", "고추튀김"] ], "lunch") +data class WidgetMealList( + val breakfast: Pair>, String>, + val lunch: Pair>, String>, + val dinner: Pair>, String>, + val restaurant: Restaurant, +) diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/theme/EATSSUWidgetColorScheme.kt b/app/src/main/java/com/eatssu/android/presentation/widget/theme/EATSSUWidgetColorScheme.kt new file mode 100644 index 000000000..b0fea1bb3 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/theme/EATSSUWidgetColorScheme.kt @@ -0,0 +1,19 @@ +package com.eatssu.android.presentation.widget.theme + +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color +import androidx.glance.material3.ColorProviders + +object EATSSUWidgetColorScheme { + + private val LightColorScheme = lightColorScheme( + primary = Color(0xFF1F1F1F), // 텍스트 기본 + onPrimary = Color(0xFFFFFFFF), // 메인 배경 + onBackground = Color(0xFFFAFAFB) // 전체 배경 + ) + + val colors = ColorProviders( + light = LightColorScheme, + dark = LightColorScheme + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/ui/MealWidget.kt b/app/src/main/java/com/eatssu/android/presentation/widget/ui/MealWidget.kt new file mode 100644 index 000000000..ef3c79cfa --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/ui/MealWidget.kt @@ -0,0 +1,315 @@ +package com.eatssu.android.presentation.widget.ui + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.action.clickable +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.cornerRadius +import androidx.glance.appwidget.lazy.LazyColumn +import androidx.glance.appwidget.lazy.itemsIndexed +import androidx.glance.appwidget.provideContent +import androidx.glance.background +import androidx.glance.currentState +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.Column +import androidx.glance.layout.Row +import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.height +import androidx.glance.layout.padding +import androidx.glance.layout.size +import androidx.glance.preview.ExperimentalGlancePreviewApi +import androidx.glance.preview.Preview +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import com.eatssu.android.R +import com.eatssu.android.data.enums.Restaurant +import com.eatssu.android.domain.model.WidgetMealInfo +import com.eatssu.android.domain.usecase.widget.LoadRestaurantByFileKeyUseCase +import com.eatssu.android.presentation.widget.MealInfoStateDefinition +import com.eatssu.android.presentation.widget.MealWorker +import com.eatssu.android.presentation.widget.theme.EATSSUWidgetColorScheme +import com.eatssu.android.presentation.widget.util.MealTime +import com.eatssu.android.presentation.widget.util.WidgetDataDisplayManager +import com.eatssu.android.presentation.widget.util.launchApp +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.delay +import timber.log.Timber + + +class MealWidget : GlanceAppWidget() { + override val stateDefinition = MealInfoStateDefinition + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface MealWidgetEntryPoint { + fun loadRestaurantByFileKeyUseCase(): LoadRestaurantByFileKeyUseCase + } + + @RequiresApi(Build.VERSION_CODES.O) + override suspend fun provideGlance(context: Context, id: GlanceId) { + provideContent { + val appContext = context.applicationContext + val entryPoint = + EntryPointAccessors.fromApplication(appContext, MealWidgetEntryPoint::class.java) + val loadRestaurantByFileKeyUseCase = entryPoint.loadRestaurantByFileKeyUseCase() + + // GlanceId -> appWidgetId 매핑 + val manager = GlanceAppWidgetManager(context) + val appWidgetId = manager.getAppWidgetId(id) + + var restaurant by remember { mutableStateOf(null) } + + LaunchedEffect(key1 = Unit) { + + delay(2000) //딜레이 안주면 Init 상태의 위젯이 추가됨. + val savedRestaurant = loadRestaurantByFileKeyUseCase( + "appWidget-${appWidgetId}" + ) + restaurant = savedRestaurant + + if (savedRestaurant != null) { + Timber.d("LaunchedEffect: 저장된 식당 정보 발견 - ${savedRestaurant.name}, 위젯 강제 업데이트") + // 위젯을 강제로 업데이트하여 저장된 식당 정보가 반영되도록 함 + MealWidget().update(context, id) + } else { + Timber.d("LaunchedEffect: 저장된 식당 정보 없음") + } + + MealWorker.enqueue(context) + } + + GlanceTheme(colors = EATSSUWidgetColorScheme.colors) { + if (restaurant != null) { + // 저장된 식당 정보가 있으면 해당 식당의 데이터 표시 + when (val state = currentState()) { + is WidgetMealInfo.Available -> { + // 현재 시간에 맞는 식사 시간의 메뉴를 표시 + val currentMealTime = WidgetDataDisplayManager.getCurrentMealTime() + val (mealTime, mealList) = when (currentMealTime) { + + MealTime.Morning -> "아침" to state.breakfast + MealTime.Lunch -> "점심" to state.lunch + MealTime.Dinner -> "저녁" to state.dinner + } + + if (mealList.isNotEmpty()) { + MealWidgetContent( + mealTime = mealTime, + mealList = mealList, + restaurant = restaurant?.displayName ?: "", + glanceId = id, + ) + } else { + MealWidgetError( + mealTime = mealTime, + restaurant = restaurant?.displayName ?: "", + text = "오늘의 메뉴가 없습니다.", + glanceId = id, + ) + } + } + + is WidgetMealInfo.Loading -> { + // Loading 상태일 때도 저장된 식당 정보 표시 + MealWidgetError( + restaurant = restaurant?.displayName ?: "", + mealTime = "점심", + text = "로딩 중", + glanceId = id, + ) + } + + is WidgetMealInfo.Unavailable -> { + MealWidgetError( + restaurant = restaurant?.displayName ?: "", + mealTime = "점심", + text = "네트워크 연결 상태를 확인해주세요.", + glanceId = id, + ) + } + } + } else { + // 저장된 식당 정보가 없으면 설정 필요 메시지 표시 + MealWidgetError( + restaurant = "설정 필요", + mealTime = "점심", + text = "위젯 설정에서 식당을 선택해주세요.", + glanceId = id, + ) + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + @Composable + private fun MealWidgetContent( + mealTime: String, + mealList: List>, + restaurant: String, + glanceId: GlanceId? = null, + ) { + val context = LocalContext.current + + MealWidgetScaffold( + context = context, + mealTime = mealTime, + restaurantName = restaurant, + content = { + LazyColumn( + modifier = GlanceModifier + .fillMaxSize() + .padding(top = 12.dp, bottom = 16.dp, start = 16.dp, end = 16.dp) + .cornerRadius(10.dp) + .background(GlanceTheme.colors.onBackground) + ) { + itemsIndexed(mealList) { index, group -> + val groupText = group.joinToString(" + ") + Column { + Text( + text = groupText, + ) + if (mealList.lastIndex != index) { + Spacer(modifier = GlanceModifier.height(8.dp)) // 그룹 간 간격 + } + } + } + } + } + ) + } + + @RequiresApi(Build.VERSION_CODES.O) + @Composable + fun MealWidgetError( + mealTime: String, + restaurant: String, + text: String, + glanceId: GlanceId? = null, + ) { + val context = LocalContext.current + + MealWidgetScaffold( + context = context, + mealTime = mealTime, + restaurantName = restaurant, + content = { + Box( + modifier = GlanceModifier + .fillMaxSize() + .padding(top = 12.dp, bottom = 16.dp, start = 16.dp, end = 16.dp) + .cornerRadius(10.dp) + .background(GlanceTheme.colors.onBackground) + ) { + Column( + modifier = GlanceModifier + .fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = GlanceModifier.size(20.dp) + .padding(bottom = 6.dp), + provider = ImageProvider(R.drawable.ic_alert_circle), + contentDescription = "alert" + ) + Text( + text, + style = TextStyle(fontSize = 10.sp, fontWeight = FontWeight.Normal), + ) + } + } + } + ) + } + + @Composable + fun MealWidgetScaffold( + context: Context, + mealTime: String?, + restaurantName: String, + content: @Composable () -> Unit + ) { + Column( + modifier = GlanceModifier.fillMaxSize() + .background(GlanceTheme.colors.onPrimary) + .padding(16.dp) + .cornerRadius(20.dp) + .clickable { + Timber.d("위젯 클릭") + context.launchApp() + }, + ) { + Row( + modifier = GlanceModifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = GlanceModifier.size(height = 14.dp, width = 43.dp), + provider = ImageProvider(R.drawable.img_new_logo_primary), + contentDescription = "Logo", + ) + Spacer(modifier = GlanceModifier.size(8.dp)) + if (mealTime != null) { + Text( + mealTime, + style = TextStyle(fontSize = 10.sp, fontWeight = FontWeight.Normal), + ) + } + Spacer(modifier = GlanceModifier.defaultWeight()) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + restaurantName, + style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Normal), + modifier = GlanceModifier.padding(start = 8.dp, end = 8.dp), + ) + } + } + Spacer(modifier = GlanceModifier.height(12.dp)) + content() + } + } + + @RequiresApi(Build.VERSION_CODES.O) + @OptIn(ExperimentalGlancePreviewApi::class) + @Preview + @Composable + fun MealWidgetPreview() { + MealWidgetContent("저녁", listOf(listOf("밥", "국", "반찬", "음료")), Restaurant.DODAM.displayName) + } + + @RequiresApi(Build.VERSION_CODES.O) + @OptIn(ExperimentalGlancePreviewApi::class) + @Preview + @Composable + fun MealWidgetPreviewError() { + MealWidgetError("저녁", Restaurant.DODAM.displayName, "에러임") + } +} diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingActivity.kt b/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingActivity.kt new file mode 100644 index 000000000..bff0fd799 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingActivity.kt @@ -0,0 +1,107 @@ +package com.eatssu.android.presentation.widget.ui + +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.annotation.RequiresApi +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.lifecycle.lifecycleScope +import com.eatssu.android.data.enums.Restaurant +import com.eatssu.android.domain.usecase.widget.SaveRestaurantByFileKeyUseCase +import com.eatssu.android.presentation.widget.MealWorker +import com.eatssu.design_system.theme.EatssuTheme +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class WidgetSettingActivity : ComponentActivity() { + + @Inject + lateinit var saveRestaurantByFileKeyUseCase: SaveRestaurantByFileKeyUseCase + + @RequiresApi(Build.VERSION_CODES.O) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + EatssuTheme { + + val restaurantOptions = Restaurant.getVariableRestaurantList().map { + it.displayName + } // 변동 식당만 불러옵니다. 하드코딩 x + + var selectedRestaurant by rememberSaveable { mutableStateOf(restaurantOptions[0]) } + + val appWidgetId = intent?.getIntExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID + ) + var glanceId by remember { mutableStateOf(null) } + val context = LocalContext.current + LaunchedEffect(appWidgetId) { + glanceId = + if (appWidgetId != null && appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId) + } else { + null + } + } + + WidgetSettingScreen( + restaurantOptionList = restaurantOptions, + selectedRestaurant = selectedRestaurant, + onSelectRestaurant = { displayName -> + selectedRestaurant = displayName + }, + onConfirm = { selectedRestaurantValue -> + if (glanceId == null) { + finish() + return@WidgetSettingScreen + } + + lifecycleScope.launch { + + saveRestaurantByFileKeyUseCase( + "appWidget-${appWidgetId}", + selectedRestaurantValue + ) + + // 위젯 업데이트 + glanceId?.let { + MealWidget().update(this@WidgetSettingActivity, it) + } + + // MealWorker 실행 + MealWorker.enqueue(this@WidgetSettingActivity) + + Timber.d("선택하기 버튼으로 저장: $selectedRestaurantValue for glanceId: $glanceId") + } + + // 결과 설정 + val resultIntent = Intent().apply { + putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + appWidgetId ?: AppWidgetManager.INVALID_APPWIDGET_ID + ) + } + setResult(RESULT_OK, resultIntent) + finish() + }, + onBack = { finish() } + ) + } + } + } +} diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingScreen.kt b/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingScreen.kt new file mode 100644 index 000000000..040d50705 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingScreen.kt @@ -0,0 +1,89 @@ +package com.eatssu.android.presentation.widget.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.eatssu.android.data.enums.Restaurant.Companion.fromDisplayName +import com.eatssu.design_system.component.EatSsuButton +import com.eatssu.design_system.component.EatSsuRadioButtonGroup +import com.eatssu.design_system.component.EatSsuTopBar +import com.eatssu.design_system.theme.EatssuTheme + +@Composable +fun WidgetSettingScreen( + modifier: Modifier = Modifier, + restaurantOptionList: List, + selectedRestaurant: String, + onSelectRestaurant: (String) -> Unit, + onConfirm: (String) -> Unit = {}, + onBack: () -> Unit = {} // 뒤로가기 동작을 위한 람다 추가 +) { + Scaffold( + modifier = modifier.fillMaxSize(), + topBar = { + EatSsuTopBar( + title = "위젯 설정", + onBack = onBack + ) + }, + content = { innerPadding -> // innerPadding 값을 받습니다. + Column( + modifier = Modifier + .padding(innerPadding) // Scafffold 패딩 적용 + .fillMaxSize() + .padding(horizontal = 24.dp) // 이후에 추가적인 패딩 적용 + ) { + Text( + text = "확인하고 싶은 식당을 선택하세요.", + style = EatssuTheme.typography.body2, + modifier = Modifier.padding(bottom = 20.dp) + ) + + EatSsuRadioButtonGroup( + options = restaurantOptionList, + selectedOption = selectedRestaurant, + onOptionSelected = { onSelectRestaurant(it) } + ) + + Spacer(modifier = Modifier.weight(1f)) + + EatSsuButton( + modifier = Modifier.padding(bottom = 74.dp), + text = "선택하기", + onClick = { + onConfirm( + fromDisplayName(selectedRestaurant) + ) + } + ) + } + } + ) +} + +@Preview(showBackground = true) +@Composable +fun PreviewWidgetSettingScreen() { + EatssuTheme { + val restaurantOptionList = listOf("학생 식당", "도담 식당", "기숙사 식당", "FACULTY(교직원 전용)") + var selectedRestaurant by remember { mutableStateOf(restaurantOptionList[0]) } + + WidgetSettingScreen( + restaurantOptionList = restaurantOptionList, + selectedRestaurant = selectedRestaurant, + onSelectRestaurant = { selectedRestaurant = it }, + onBack = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/util/WidgetDataDisplayManager.kt b/app/src/main/java/com/eatssu/android/presentation/widget/util/WidgetDataDisplayManager.kt new file mode 100644 index 000000000..9d3060592 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/util/WidgetDataDisplayManager.kt @@ -0,0 +1,124 @@ +package com.eatssu.android.presentation.widget.util + +import android.os.Build +import androidx.annotation.RequiresApi +import com.eatssu.android.data.enums.Restaurant +import com.eatssu.android.domain.model.WidgetMealInfo +import com.eatssu.android.domain.usecase.widget.GetTodayMealUseCase +import com.eatssu.android.domain.usecase.widget.MealState +import com.eatssu.android.presentation.util.CalendarUtil +import com.eatssu.android.presentation.widget.WidgetCacheManager +import timber.log.Timber +import java.time.LocalTime + +sealed class MealTime { + + data object Morning : MealTime() + + data object Lunch : MealTime() + + data object Dinner : MealTime() +} + +sealed class MealInfoState { + + data object Loading : MealInfoState() + + data class Available( + val mealTime: String, + val mealList: List>, + val restaurant: Restaurant, + ) : MealInfoState() + + data object Unavailable : MealInfoState() +} + +object WidgetDataDisplayManager { + + @RequiresApi(Build.VERSION_CODES.O) + internal suspend fun fetchMealInfo( + getMealsUseCase: GetTodayMealUseCase, + requestedMealTime: MealTime, + restaurant: Restaurant, + ): WidgetMealInfo { + Timber.d("Widget - fetchMealInfo") + val targetDate = CalendarUtil.convertMillisToDateString(System.currentTimeMillis()) + + // 캐시에서 데이터 확인 + val cachedMealInfo = WidgetCacheManager.getCachedMealData(restaurant, targetDate) + if (cachedMealInfo != null) { + return cachedMealInfo + } + + val response = getMealsUseCase(targetDate, restaurant.name) + Timber.d("Widget - fetchMealInfo $response") + + if (response is MealState.Success) { + val breakfast = response.response.breakfast.first + val lunch = response.response.lunch.first + val dinner = response.response.dinner.first + + // 3개 식사 시간을 모두 저장 + val mealInfo = WidgetMealInfo.Available( + breakfast = breakfast, + lunch = lunch, + dinner = dinner, + restaurant = restaurant + ) + + // 캐시에 저장 + WidgetCacheManager.cacheMealData(restaurant, mealInfo, targetDate) + + return mealInfo + } + + // 다음 날 데이터 확인 + val nextDay = CalendarUtil.getNextDayDate() + val getNextDayMealResponse = getMealsUseCase(nextDay, restaurant.name) + + if (getNextDayMealResponse is MealState.Success) { + val breakfast = getNextDayMealResponse.response.breakfast.first + val lunch = getNextDayMealResponse.response.lunch.first + val dinner = getNextDayMealResponse.response.dinner.first + + // 3개 식사 시간을 모두 저장 + val mealInfo = WidgetMealInfo.Available( + breakfast = breakfast, + lunch = lunch, + dinner = dinner, + restaurant = restaurant + ) + + // 캐시에 저장 + WidgetCacheManager.cacheMealData(restaurant, mealInfo, targetDate) + + return mealInfo + } + + // 모든 시간대의 식단이 비어있는 경우 + val emptyMealInfo = WidgetMealInfo.Available( + breakfast = emptyList(), + lunch = emptyList(), + dinner = emptyList(), + restaurant = restaurant + ) + + // 캐시에 저장 + WidgetCacheManager.cacheMealData(restaurant, emptyMealInfo, targetDate) + + return emptyMealInfo + } + + @RequiresApi(Build.VERSION_CODES.O) + internal fun getCurrentMealTime(): MealTime { + val currentTime = LocalTime.now() + val morningEnd = LocalTime.of(9, 0) + val lunchEnd = LocalTime.of(15, 0) + + return when { + currentTime.isBefore(morningEnd) -> MealTime.Morning + currentTime.isBefore(lunchEnd) -> MealTime.Lunch + else -> MealTime.Dinner + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/util/launchApp.kt b/app/src/main/java/com/eatssu/android/presentation/widget/util/launchApp.kt new file mode 100644 index 000000000..1d4fe2e37 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/widget/util/launchApp.kt @@ -0,0 +1,13 @@ +package com.eatssu.android.presentation.widget.util + +import android.content.Context +import android.content.Intent +import android.net.Uri + +fun Context.launchApp() { + val deepLinkUri = Uri.parse("eatssu://root") + val intent = Intent(Intent.ACTION_VIEW, deepLinkUri) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + runCatching { this.startActivity(intent) } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_alert_circle.xml b/app/src/main/res/drawable/ic_alert_circle.xml new file mode 100644 index 000000000..1b7db65d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_alert_circle.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_alarm_logo.xml b/app/src/main/res/drawable/ic_mini_logo.xml similarity index 100% rename from app/src/main/res/drawable/ic_alarm_logo.xml rename to app/src/main/res/drawable/ic_mini_logo.xml diff --git a/app/src/main/res/drawable/ic_mini_logo_mint.xml b/app/src/main/res/drawable/ic_mini_logo_mint.xml new file mode 100644 index 000000000..7109ad337 --- /dev/null +++ b/app/src/main/res/drawable/ic_mini_logo_mint.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/img_widget_preview.png b/app/src/main/res/drawable/img_widget_preview.png new file mode 100644 index 000000000..555234951 Binary files /dev/null and b/app/src/main/res/drawable/img_widget_preview.png differ diff --git a/app/src/main/res/xml-v31/widget_info.xml b/app/src/main/res/xml-v31/widget_info.xml new file mode 100644 index 000000000..daf5fac73 --- /dev/null +++ b/app/src/main/res/xml-v31/widget_info.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/xml/widget_info.xml b/app/src/main/res/xml/widget_info.xml new file mode 100644 index 000000000..d438775bf --- /dev/null +++ b/app/src/main/res/xml/widget_info.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/core/design-system/.gitignore b/core/design-system/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/design-system/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/design-system/build.gradle.kts b/core/design-system/build.gradle.kts new file mode 100644 index 000000000..1c27707eb --- /dev/null +++ b/core/design-system/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.eatssu.design_system" + compileSdk = 35 + + defaultConfig { + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.15" + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.espresso.core) + + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.animation) + implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.lifecycle.viewmodel) + implementation(libs.androidx.compose.lifecycle.runtime) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + +} \ No newline at end of file diff --git a/core/design-system/consumer-rules.pro b/core/design-system/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/design-system/proguard-rules.pro b/core/design-system/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/design-system/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/design-system/src/main/java/com/eatssu/design_system/component/EatSsuButton.kt b/core/design-system/src/main/java/com/eatssu/design_system/component/EatSsuButton.kt new file mode 100644 index 000000000..52c25f840 --- /dev/null +++ b/core/design-system/src/main/java/com/eatssu/design_system/component/EatSsuButton.kt @@ -0,0 +1,50 @@ +package com.eatssu.design_system.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.eatssu.design_system.theme.EatssuTheme + +@Composable +fun EatSsuButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .height(50.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = Color.White + ), + ) { + Text( + text = text, + modifier = Modifier, +// .padding(vertical = 13.dp), + style = EatssuTheme.typography.button1, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewRoundedSelectButton() { + + EatssuTheme { + EatSsuButton(text = "선택하기", onClick = { /* Button Clicked */ }) + } +} diff --git a/core/design-system/src/main/java/com/eatssu/design_system/component/EatSsuRadioButton.kt b/core/design-system/src/main/java/com/eatssu/design_system/component/EatSsuRadioButton.kt new file mode 100644 index 000000000..38f80a92d --- /dev/null +++ b/core/design-system/src/main/java/com/eatssu/design_system/component/EatSsuRadioButton.kt @@ -0,0 +1,86 @@ +package com.eatssu.design_system.component + + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun EatSsuRadioButton( + text: String, + isSelected: Boolean, + onSelect: () -> Unit +) { + val backgroundColor = + if (isSelected) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.surface + val borderColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.LightGray + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .background(backgroundColor) + .border(BorderStroke(1.dp, borderColor), RoundedCornerShape(10.dp)) + .clickable { onSelect() } + .padding(15.dp), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = text, + color = MaterialTheme.colorScheme.onSurface, + style = com.eatssu.design_system.theme.EatssuTheme.typography.body2 + ) + } +} + +@Composable +fun EatSsuRadioButtonGroup( + options: List, + selectedOption: String, + onOptionSelected: (String) -> Unit +) { + LazyColumn(modifier = Modifier.fillMaxWidth()) { + items(options, key = { it }) { option -> + val isSelected = option == selectedOption + EatSsuRadioButton( + text = option, + isSelected = isSelected, + onSelect = { onOptionSelected(option) } + ) + Spacer(modifier = Modifier.height(12.dp)) + } + } +} + + +@Composable +@Preview +fun RestaurantSelectionPreview() { + val restaurantOptions = listOf("학생 식당", "도담 식당", "기숙사 식당") + + com.eatssu.design_system.theme.EatssuTheme { + EatSsuRadioButtonGroup( + options = restaurantOptions, + selectedOption = restaurantOptions[0], + onOptionSelected = {} + ) + } +} \ No newline at end of file diff --git a/core/design-system/src/main/java/com/eatssu/design_system/component/EatSsuTopBar.kt b/core/design-system/src/main/java/com/eatssu/design_system/component/EatSsuTopBar.kt new file mode 100644 index 000000000..faa6cc695 --- /dev/null +++ b/core/design-system/src/main/java/com/eatssu/design_system/component/EatSsuTopBar.kt @@ -0,0 +1,53 @@ +package com.eatssu.design_system.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.eatssu.design_system.R +import com.eatssu.design_system.theme.EatssuTheme +import com.eatssu.design_system.theme.Gray500 + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun EatSsuTopBar( + title: String, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + CenterAlignedTopAppBar( + modifier = modifier.fillMaxWidth(), + title = { + Text( + text = title, + style = EatssuTheme.typography.subtitle1 + ) + }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_left), + contentDescription = "뒤로가기", + tint = Gray500 + ) + } + } + ) +} + +@Preview +@Composable +fun PreviewTopBar() { + EatssuTheme { + Column { + EatSsuTopBar("리뷰", {}) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/compose/ui/theme/Color.kt b/core/design-system/src/main/java/com/eatssu/design_system/theme/Color.kt similarity index 90% rename from app/src/main/java/com/eatssu/android/presentation/compose/ui/theme/Color.kt rename to core/design-system/src/main/java/com/eatssu/design_system/theme/Color.kt index b7c6d2a00..bd754fb53 100644 --- a/app/src/main/java/com/eatssu/android/presentation/compose/ui/theme/Color.kt +++ b/core/design-system/src/main/java/com/eatssu/design_system/theme/Color.kt @@ -1,4 +1,4 @@ -package com.eatssu.android.presentation.compose.ui.theme +package com.eatssu.design_system.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/com/eatssu/android/presentation/compose/ui/theme/Theme.kt b/core/design-system/src/main/java/com/eatssu/design_system/theme/Theme.kt similarity index 97% rename from app/src/main/java/com/eatssu/android/presentation/compose/ui/theme/Theme.kt rename to core/design-system/src/main/java/com/eatssu/design_system/theme/Theme.kt index 8fca0e807..31f0cbe5c 100644 --- a/app/src/main/java/com/eatssu/android/presentation/compose/ui/theme/Theme.kt +++ b/core/design-system/src/main/java/com/eatssu/design_system/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.eatssu.android.presentation.compose.ui.theme +package com.eatssu.design_system.theme import android.app.Activity import androidx.compose.material3.MaterialTheme diff --git a/app/src/main/java/com/eatssu/android/presentation/compose/ui/theme/Type.kt b/core/design-system/src/main/java/com/eatssu/design_system/theme/Type.kt similarity index 97% rename from app/src/main/java/com/eatssu/android/presentation/compose/ui/theme/Type.kt rename to core/design-system/src/main/java/com/eatssu/design_system/theme/Type.kt index 9cc58e3c1..d43f3acc6 100644 --- a/app/src/main/java/com/eatssu/android/presentation/compose/ui/theme/Type.kt +++ b/core/design-system/src/main/java/com/eatssu/design_system/theme/Type.kt @@ -1,4 +1,4 @@ -package com.eatssu.android.presentation.compose.ui.theme +package com.eatssu.design_system.theme import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -9,7 +9,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp -import com.eatssu.android.R +import com.eatssu.design_system.R // 폰트 패밀리 설정 val pretendardBold = FontFamily(Font(R.font.pretendard_bold, FontWeight.Bold)) diff --git a/core/design-system/src/main/res/drawable/ic_arrow_left.xml b/core/design-system/src/main/res/drawable/ic_arrow_left.xml new file mode 100644 index 000000000..2aec97b71 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_arrow_left.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/font-v26/font.xml b/core/design-system/src/main/res/font/font.xml similarity index 100% rename from app/src/main/res/font-v26/font.xml rename to core/design-system/src/main/res/font/font.xml diff --git a/app/src/main/res/font/pretendard_black.ttf b/core/design-system/src/main/res/font/pretendard_black.ttf similarity index 100% rename from app/src/main/res/font/pretendard_black.ttf rename to core/design-system/src/main/res/font/pretendard_black.ttf diff --git a/app/src/main/res/font/pretendard_bold.ttf b/core/design-system/src/main/res/font/pretendard_bold.ttf similarity index 100% rename from app/src/main/res/font/pretendard_bold.ttf rename to core/design-system/src/main/res/font/pretendard_bold.ttf diff --git a/app/src/main/res/font/pretendard_extrabold.ttf b/core/design-system/src/main/res/font/pretendard_extrabold.ttf similarity index 100% rename from app/src/main/res/font/pretendard_extrabold.ttf rename to core/design-system/src/main/res/font/pretendard_extrabold.ttf diff --git a/app/src/main/res/font/pretendard_extralight.ttf b/core/design-system/src/main/res/font/pretendard_extralight.ttf similarity index 100% rename from app/src/main/res/font/pretendard_extralight.ttf rename to core/design-system/src/main/res/font/pretendard_extralight.ttf diff --git a/app/src/main/res/font/pretendard_light.ttf b/core/design-system/src/main/res/font/pretendard_light.ttf similarity index 100% rename from app/src/main/res/font/pretendard_light.ttf rename to core/design-system/src/main/res/font/pretendard_light.ttf diff --git a/app/src/main/res/font/pretendard_medium.ttf b/core/design-system/src/main/res/font/pretendard_medium.ttf similarity index 100% rename from app/src/main/res/font/pretendard_medium.ttf rename to core/design-system/src/main/res/font/pretendard_medium.ttf diff --git a/app/src/main/res/font/pretendard_regular.ttf b/core/design-system/src/main/res/font/pretendard_regular.ttf similarity index 100% rename from app/src/main/res/font/pretendard_regular.ttf rename to core/design-system/src/main/res/font/pretendard_regular.ttf diff --git a/app/src/main/res/font/pretendard_semibold.ttf b/core/design-system/src/main/res/font/pretendard_semibold.ttf similarity index 100% rename from app/src/main/res/font/pretendard_semibold.ttf rename to core/design-system/src/main/res/font/pretendard_semibold.ttf diff --git a/app/src/main/res/font/pretendard_thin.ttf b/core/design-system/src/main/res/font/pretendard_thin.ttf similarity index 100% rename from app/src/main/res/font/pretendard_thin.ttf rename to core/design-system/src/main/res/font/pretendard_thin.ttf diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 52b2d5ab3..7d3c569f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,10 @@ [versions] accompanistAppcompatTheme = "0.16.0" -activityCompose = "1.9.2" android = "8.6.1" androidx-core = "1.7.0" androidx-appcompat = "1.6.1" +hiltAndroidCompiler = "2.42" +lifecycleRuntimeCompose = "2.8.7" animation = "1.7.8" composeBomVersion = "2025.03.00" composeThemeAdapter = "1.2.1" @@ -12,6 +13,7 @@ lifecycleViewmodelCompose = "2.8.7" material = "1.8.0" constraintlayout = "2.1.4" materialVersion = "1.7.8" +navigationFragment = "2.9.3" threetenabp = "1.4.4" material-calendarview = "1.4.3" recyclerview = "1.3.2" @@ -22,6 +24,15 @@ espresso-core = "3.5.1" activity = "1.8.2" fragment = "1.6.2" databinding-compiler = "3.1.4" +uiTestJunit4 = "1.7.8" +uiTooling = "1.7.8" +compose-material3 = "1.3.1" +activityVersion = "1.9.3" +workRuntimeKtx = "2.10.0" +datastore = "1.0.0" +activityCompose = "1.10.1" +composeBom = "2024.04.01" +lifecycle = "2.7.0" retrofit = "2.9.0" converter-gson = "2.9.0" gson = "2.10.1" @@ -30,91 +41,104 @@ glide = "4.15.1" glide-compiler = "4.12.0" compressor = "3.0.1" coroutines = "1.7.3" -lifecycle-runtime = "2.7.0" -lifecycle-viewmodel = "2.7.0" -lifecycle-livedata = "2.7.0" kakao-login = "2.8.6" hilt = "2.50" +androidxHilt = "1.2.0" play-services-base = "18.0.1" firebase-bom = "32.6.0" firebase-crashlytics = "2.9.9" -timber = "5.0.1" google-services = "4.4.2" +timber = "5.0.1" kotlin-android = "1.9.25" +ksp = "1.9.0-1.0.13" ossLicenses = "17.1.0" ossLicensesPlugin = "0.10.4" -uiTestJunit4 = "1.7.8" -uiTooling = "1.7.8" -compose-material3 = "1.3.1" -activityVersion = "1.9.3" +glanceAppwidget = "1.1.1" +glanceAppwidgetPreview = "1.1.1" +glancePreview = "1.1.1" [libraries] -accompanist-appcompat-theme = { module = "com.google.accompanist:accompanist-appcompat-theme", version.ref = "accompanistAppcompatTheme" } -androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } -androidx-animation = { module = "androidx.compose.animation:animation", version.ref = "animation" } +# kotlinx, kotlin +androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigationFragment" } +androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "navigationFragment" } + +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } + +# androidx +androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } -androidx-lifecycle-runtime-ktx-v252 = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } -androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } -androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } -androidx-ui = { module = "androidx.compose.ui:ui" } -androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } -androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "uiTestJunit4" } -androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } -androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "uiTooling" } -androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } -compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBomVersion" } -compose-theme-adapter = { module = "com.google.android.material:compose-theme-adapter", version.ref = "composeThemeAdapter" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } -constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } -threetenabp = { group = "com.jakewharton.threetenabp", name = "threetenabp", version.ref = "threetenabp" } -material-calendarview = { group = "com.prolificinteractive", name = "material-calendarview", version.ref = "material-calendarview" } -recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" } -transport-runtime = { group = "com.google.android.datatransport", name = "transport-runtime", version.ref = "transport-runtime" } -junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" } +androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" } +androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment" } +androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } +androidx-hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "androidxHilt" } +androidx-hilt-common = { group = "androidx.hilt", name = "hilt-common", version.ref = "androidxHilt" } +androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "androidxHilt" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } -espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } -activity = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" } -fragment = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment" } -databinding-compiler = { group = "com.android.databinding", name = "compiler", version.ref = "databinding-compiler" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } -retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } -converter_gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "converter-gson" } +# compose +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } +androidx-compose-animation = { module = "androidx.compose.animation:animation", version.ref = "animation" } +androidx-compose-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } +androidx-compose-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } -gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } -okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } -okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +# glance +androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glanceAppwidget" } +androidx-glance-appwidget-preview = { group = "androidx.glance", name = "glance-appwidget-preview", version.ref = "glanceAppwidgetPreview" } +androidx-glance-preview = { group = "androidx.glance", name = "glance-preview", version.ref = "glancePreview" } +androidx-glance-material3 = { group = "androidx.glance", name = "glance-material3", version.ref = "glanceAppwidget" } +androidx-glance = { group = "androidx.glance", name = "glance", version.ref = "glanceAppwidget" } +# glide glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" } -glide_compiler = { group = "com.github.bumptech.glide", name = "compiler", version.ref = "glide-compiler" } -compressor = { group = "id.zelory", name = "compressor", version.ref = "compressor" } - -coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } -coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } - -lifecycle_runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime" } -lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle-viewmodel" } -lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle-livedata" } - -kakao-login = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao-login" } +glide-compiler = { group = "com.github.bumptech.glide", name = "compiler", version.ref = "glide-compiler" } -hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } -hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } +# retrofit, okhttp, gson +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "converter-gson" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } -play_services_base = { group = "com.google.android.gms", name = "play-services-base", version.ref = "play-services-base" } +# hilt +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } -firebase-config = { module = "com.google.firebase:firebase-config" } -firebase-analytics = { module = "com.google.firebase:firebase-analytics" } -firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } +# firebase +firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" } +firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } +firebase-config = { group = "com.google.firebase", name = "firebase-config" } +firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" } +play-services-base = { group = "com.google.android.gms", name = "play-services-base", version.ref = "play-services-base" } +# etc +accompanist-appcompat-theme = { group = "com.google.accompanist", name = "accompanist-appcompat-theme", version.ref = "accompanistAppcompatTheme" } +compose-theme-adapter = { group = "com.google.android.material", name = "compose-theme-adapter", version.ref = "composeThemeAdapter" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +threetenabp = { group = "com.jakewharton.threetenabp", name = "threetenabp", version.ref = "threetenabp" } +material-calendarview = { group = "com.prolificinteractive", name = "material-calendarview", version.ref = "material-calendarview" } +transport-runtime = { group = "com.google.android.datatransport", name = "transport-runtime", version.ref = "transport-runtime" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +compressor = { group = "id.zelory", name = "compressor", version.ref = "compressor" } +kakao-login = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao-login" } timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } oss-licenses = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "ossLicenses" } oss-licenses-plugin = { group = "com.google.android.gms", name = "oss-licenses-plugin", version.ref = "ossLicensesPlugin" } -androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activityVersion" } - - - [plugins] android-application = { id = "com.android.application", version.ref = "android" } diff --git a/settings.gradle b/settings.gradle index ab65a43c1..e6ef2d26b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,3 +16,4 @@ dependencyResolutionManagement { } rootProject.name = "EatSSU-Android" include ':app' +include ':core:design-system'