diff --git a/app/build.gradle b/app/build.gradle index 95bf3092e..7b0b596c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,7 +13,26 @@ Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) def NATIVE_APP_KEY = properties.getProperty('NATIVE_APP_KEY') def ADS_APPLICATION_ID = properties.getProperty('ADS_APPLICATION_ID') +def REMOTE_CONFIG_BASE_URL_PUBLIC_KEY = properties.getProperty('REMOTE_CONFIG_BASE_URL_PUBLIC_KEY', '').trim() def keystorePropertiesFile = rootProject.file("app/keystore-release.properties") +def buildConfigString = { String value -> + def escaped = (value ?: '') + .replace('\\', '\\\\') + .replace('"', '\\"') + .replace('\n', '\\n') + return "\"${escaped}\"" +} +def fallbackBaseUrlProperty = { String fallbackKey, String legacyKey -> + def value = properties.getProperty(fallbackKey) ?: properties.getProperty(legacyKey) + if (!value) { + throw new GradleException("Missing ${fallbackKey} in local.properties") + } + return value +} +def FALLBACK_BASE_URL_DEV = fallbackBaseUrlProperty('FALLBACK_BASE_URL_DEV', 'BASE_URL_DEV') +def FALLBACK_BASE_URL_PROD = fallbackBaseUrlProperty('FALLBACK_BASE_URL_PROD', 'BASE_URL_PROD') +def USES_DEV_CLEARTEXT_TRAFFIC = FALLBACK_BASE_URL_DEV?.contains('http://') == true +def USES_PROD_CLEARTEXT_TRAFFIC = FALLBACK_BASE_URL_PROD?.contains('http://') == true kotlin { jvmToolchain(17) @@ -35,9 +54,11 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField ("String", "NATIVE_APP_KEY", properties['NATIVE_APP_KEY_STR']) + buildConfigField("String", "REMOTE_CONFIG_BASE_URL_PUBLIC_KEY", buildConfigString(REMOTE_CONFIG_BASE_URL_PUBLIC_KEY)) manifestPlaceholders = [ - NATIVE_APP_KEY : NATIVE_APP_KEY, - ADS_APPLICATION_ID: ADS_APPLICATION_ID + NATIVE_APP_KEY : NATIVE_APP_KEY, + ADS_APPLICATION_ID : ADS_APPLICATION_ID, + USES_CLEARTEXT_TRAFFIC: false ] } @@ -64,6 +85,7 @@ android { applicationIdSuffix ".debug" versionNameSuffix "-debug" resValue "string", "app_name", "DAYO (Debug)" + manifestPlaceholders.USES_CLEARTEXT_TRAFFIC = true } release { resValue "string", "app_name", "DAYO" @@ -76,16 +98,40 @@ android { productFlavors { dev { dimension "environment" - buildConfigField("String", "BASE_URL", properties['BASE_URL_DEV']) + manifestPlaceholders.USES_CLEARTEXT_TRAFFIC = USES_DEV_CLEARTEXT_TRAFFIC + buildConfigField("String", "FALLBACK_BASE_URL", FALLBACK_BASE_URL_DEV) + buildConfigField("String", "REMOTE_CONFIG_BASE_URL_KEY", "\"api_base_url_dev\"") + buildConfigField("String", "REMOTE_CONFIG_BASE_URL_SIGNATURE_KEY", "\"api_base_url_dev_signature\"") } prod { dimension "environment" - buildConfigField("String", "BASE_URL", properties['BASE_URL_PROD']) + manifestPlaceholders.USES_CLEARTEXT_TRAFFIC = USES_PROD_CLEARTEXT_TRAFFIC + buildConfigField("String", "FALLBACK_BASE_URL", FALLBACK_BASE_URL_PROD) + buildConfigField("String", "REMOTE_CONFIG_BASE_URL_KEY", "\"api_base_url_prod\"") + buildConfigField("String", "REMOTE_CONFIG_BASE_URL_SIGNATURE_KEY", "\"api_base_url_prod_signature\"") } } namespace 'com.daily.dayo' } +def validateRemoteConfigBaseUrlPublicKeyForRelease = tasks.register( + 'validateRemoteConfigBaseUrlPublicKeyForRelease' +) { + doLast { + if (!REMOTE_CONFIG_BASE_URL_PUBLIC_KEY) { + throw new GradleException( + 'Missing REMOTE_CONFIG_BASE_URL_PUBLIC_KEY in local.properties for release build' + ) + } + } +} + +tasks.configureEach { task -> + if (task.name ==~ /^pre.*ReleaseBuild$/) { + task.dependsOn(validateRemoteConfigBaseUrlPublicKeyForRelease) + } +} + dependencies { // multiModule implementation project(':presentation') @@ -107,7 +153,8 @@ dependencies { implementation platform('com.google.firebase:firebase-bom:34.6.0') implementation "com.google.firebase:firebase-crashlytics" implementation "com.google.firebase:firebase-analytics" + implementation "com.google.firebase:firebase-config" // Google Ads implementation 'com.google.android.gms:play-services-ads:24.7.0' -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0976f302a..b0c441d73 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ - \ No newline at end of file + diff --git a/app/src/main/java/com/daily/dayo/config/RemoteConfigBaseUrlProvider.kt b/app/src/main/java/com/daily/dayo/config/RemoteConfigBaseUrlProvider.kt new file mode 100644 index 000000000..82ec8ca50 --- /dev/null +++ b/app/src/main/java/com/daily/dayo/config/RemoteConfigBaseUrlProvider.kt @@ -0,0 +1,262 @@ +package com.daily.dayo.config + +import android.content.Context +import android.content.SharedPreferences +import com.daily.dayo.BuildConfig +import com.google.android.gms.tasks.Task +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings +import daily.dayo.domain.provider.BaseUrlSource +import daily.dayo.domain.provider.BaseUrlProvider +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import java.net.InetAddress +import java.net.URI +import java.security.KeyFactory +import java.security.Signature +import java.security.spec.X509EncodedKeySpec +import java.util.Base64 +import java.util.Locale +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class RemoteConfigBaseUrlProvider( + context: Context, + private val remoteConfig: FirebaseRemoteConfig +) : BaseUrlProvider { + + private val sharedPreferences: SharedPreferences = context.getSharedPreferences( + PREFERENCES_NAME, + Context.MODE_PRIVATE + ) + + @Volatile + private var currentBaseUrlState: BaseUrlState = getInitialBaseUrlState() + + init { + remoteConfig.setConfigSettingsAsync( + FirebaseRemoteConfigSettings.Builder() + .setFetchTimeoutInSeconds(FETCH_TIMEOUT_SECONDS) + .setMinimumFetchIntervalInSeconds(MINIMUM_FETCH_INTERVAL_SECONDS) + .build() + ) + remoteConfig.setDefaultsAsync( + mapOf( + BuildConfig.REMOTE_CONFIG_BASE_URL_KEY to BuildConfig.FALLBACK_BASE_URL, + BuildConfig.REMOTE_CONFIG_BASE_URL_SIGNATURE_KEY to "" + ) + ) + } + + override fun getBaseUrl(): String = currentBaseUrlState.baseUrl + + override fun getBaseUrlSource(): BaseUrlSource = currentBaseUrlState.source + + override fun isTrustedBaseUrl(baseUrl: String): Boolean { + val normalizedBaseUrl = normalizeBaseUrl(baseUrl) ?: return false + return normalizedBaseUrl == currentBaseUrlState.baseUrl || + normalizedBaseUrl == normalizeBaseUrl(BuildConfig.FALLBACK_BASE_URL) + } + + override suspend fun refreshBaseUrl(): Boolean { + return try { + remoteConfig.fetchAndActivate().awaitWithTimeout() ?: return false + val remoteBaseUrlValue = remoteConfig.getValue(BuildConfig.REMOTE_CONFIG_BASE_URL_KEY) + if (remoteBaseUrlValue.source != FirebaseRemoteConfig.VALUE_SOURCE_REMOTE) { + return false + } + + val remoteBaseUrl = remoteBaseUrlValue.asString() + val remoteBaseUrlSignature = remoteConfig.getString(BuildConfig.REMOTE_CONFIG_BASE_URL_SIGNATURE_KEY) + val normalizedBaseUrl = normalizeBaseUrl(remoteBaseUrl) ?: run { + return false + } + if (!isTrustedRemoteBaseUrl(normalizedBaseUrl, remoteBaseUrlSignature)) { + return false + } + + currentBaseUrlState = BaseUrlState( + baseUrl = normalizedBaseUrl, + source = BaseUrlSource.REMOTE_CONFIG + ) + sharedPreferences.edit() + .putString(KEY_LAST_SUCCESSFUL_BASE_URL, normalizedBaseUrl) + .putString(KEY_LAST_SUCCESSFUL_BASE_URL_SIGNATURE, remoteBaseUrlSignature) + .apply() + true + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + false + } + } + + private fun getInitialBaseUrlState(): BaseUrlState { + val cachedBaseUrl = sharedPreferences.getString(KEY_LAST_SUCCESSFUL_BASE_URL, null) + val cachedBaseUrlSignature = sharedPreferences.getString(KEY_LAST_SUCCESSFUL_BASE_URL_SIGNATURE, null) + normalizeBaseUrl(cachedBaseUrl) + ?.takeIf { baseUrl -> isTrustedCachedBaseUrl(baseUrl, cachedBaseUrlSignature) } + ?.let { baseUrl -> + return BaseUrlState( + baseUrl = baseUrl, + source = BaseUrlSource.CACHED_LAST_SUCCESS + ) + } + if (cachedBaseUrl != null) { + sharedPreferences.edit() + .remove(KEY_LAST_SUCCESSFUL_BASE_URL) + .remove(KEY_LAST_SUCCESSFUL_BASE_URL_SIGNATURE) + .apply() + } + + return BaseUrlState( + baseUrl = normalizeBaseUrl(BuildConfig.FALLBACK_BASE_URL) ?: BuildConfig.FALLBACK_BASE_URL, + source = BaseUrlSource.FALLBACK + ) + } + + private suspend fun Task.awaitWithTimeout(): Boolean? { + return withTimeoutOrNull(TASK_TIMEOUT_MILLIS) { + suspendCancellableCoroutine { continuation -> + addOnCompleteListener { task -> + if (!continuation.isActive) return@addOnCompleteListener + + val exception = task.exception + if (exception != null) { + continuation.resumeWithException(exception) + } else { + continuation.resume(task.result == true) + } + } + } + } + } + + private fun normalizeBaseUrl(baseUrl: String?): String? { + val trimmedBaseUrl = baseUrl?.trim().orEmpty() + if (trimmedBaseUrl.isEmpty()) return null + + return try { + val uri = URI(trimmedBaseUrl) + if (uri.rawQuery != null || uri.rawFragment != null) return null + + val scheme = uri.scheme?.lowercase(Locale.US) ?: return null + val host = uri.host?.takeIf { it.isNotBlank() }?.lowercase(Locale.US) ?: return null + if (scheme != SCHEME_HTTP && scheme != SCHEME_HTTPS) return null + if (!BuildConfig.DEBUG && scheme == SCHEME_HTTP && !isReleaseHttpAllowed()) return null + if (uri.userInfo != null) return null + if (!BuildConfig.DEBUG && isUnsafeReleaseHost(host)) return null + + val path = uri.path.orEmpty() + if (path.isNotBlank() && path != "/") return null + + val canonicalPort = when { + scheme == SCHEME_HTTP && uri.port == DEFAULT_HTTP_PORT -> -1 + scheme == SCHEME_HTTPS && uri.port == DEFAULT_HTTPS_PORT -> -1 + else -> uri.port + } + + val normalizedUri = URI(scheme, null, host, canonicalPort, "/", null, null) + normalizedUri.toString().ensureTrailingSlash() + } catch (exception: Exception) { + null + } + } + + private fun isTrustedRemoteBaseUrl(normalizedBaseUrl: String, signature: String): Boolean { + if (normalizedBaseUrl == normalizeBaseUrl(BuildConfig.FALLBACK_BASE_URL)) return true + if (BuildConfig.DEBUG) return true + + return verifyBaseUrlSignature( + baseUrl = normalizedBaseUrl, + signature = signature + ) + } + + private fun isTrustedCachedBaseUrl(normalizedBaseUrl: String, signature: String?): Boolean { + if (normalizedBaseUrl == normalizeBaseUrl(BuildConfig.FALLBACK_BASE_URL)) return true + if (BuildConfig.DEBUG) return true + + return verifyBaseUrlSignature( + baseUrl = normalizedBaseUrl, + signature = signature.orEmpty() + ) + } + + private fun verifyBaseUrlSignature(baseUrl: String, signature: String): Boolean { + val publicKeyText = BuildConfig.REMOTE_CONFIG_BASE_URL_PUBLIC_KEY.stripPemFormatting() + val signatureText = signature.stripPemFormatting() + if (publicKeyText.isBlank() || signatureText.isBlank()) return false + + return runCatching { + val publicKey = KeyFactory.getInstance(KEY_ALGORITHM_RSA).generatePublic( + X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyText)) + ) + val verifier = Signature.getInstance(SIGNATURE_ALGORITHM) + verifier.initVerify(publicKey) + verifier.update(baseUrl.toByteArray(Charsets.UTF_8)) + verifier.verify(Base64.getDecoder().decode(signatureText)) + }.getOrDefault(false) + } + + private fun isReleaseHttpAllowed(): Boolean = + runCatching { + URI(BuildConfig.FALLBACK_BASE_URL).scheme?.lowercase(Locale.US) == SCHEME_HTTP + }.getOrDefault(false) + + private fun isUnsafeReleaseHost(host: String): Boolean { + val normalizedHost = host.trim('[', ']') + if (normalizedHost.equals("localhost", ignoreCase = true)) return true + if (normalizedHost.endsWith(".localhost", ignoreCase = true)) return true + if (normalizedHost.endsWith(".local", ignoreCase = true)) return true + if (!normalizedHost.looksLikeIpLiteral()) return false + + return runCatching { + val address = InetAddress.getByName(normalizedHost) + address.isAnyLocalAddress || + address.isLoopbackAddress || + address.isLinkLocalAddress || + address.isSiteLocalAddress || + address.isMulticastAddress + }.getOrDefault(true) + } + + private fun String.looksLikeIpLiteral(): Boolean = + IPV4_ADDRESS_REGEX.matches(this) || contains(':') + + private fun String.stripPemFormatting(): String = + replace(PEM_PUBLIC_KEY_BEGIN, "") + .replace(PEM_PUBLIC_KEY_END, "") + .replace(PEM_SIGNATURE_BEGIN, "") + .replace(PEM_SIGNATURE_END, "") + .filterNot { it.isWhitespace() } + + private fun String.ensureTrailingSlash(): String = + if (endsWith('/')) this else "$this/" + + private data class BaseUrlState( + val baseUrl: String, + val source: BaseUrlSource + ) + + companion object { + private const val PREFERENCES_NAME = "remote_config_base_url" + private const val KEY_LAST_SUCCESSFUL_BASE_URL = "last_successful_base_url" + private const val KEY_LAST_SUCCESSFUL_BASE_URL_SIGNATURE = "last_successful_base_url_signature" + private const val FETCH_TIMEOUT_SECONDS = 5L + private const val TASK_TIMEOUT_MILLIS = 6_000L + private val MINIMUM_FETCH_INTERVAL_SECONDS = if (BuildConfig.DEBUG) 0L else 3_600L + private const val SCHEME_HTTP = "http" + private const val SCHEME_HTTPS = "https" + private const val DEFAULT_HTTP_PORT = 80 + private const val DEFAULT_HTTPS_PORT = 443 + private const val KEY_ALGORITHM_RSA = "RSA" + private const val SIGNATURE_ALGORITHM = "SHA256withRSA" + private const val PEM_PUBLIC_KEY_BEGIN = "-----BEGIN PUBLIC KEY-----" + private const val PEM_PUBLIC_KEY_END = "-----END PUBLIC KEY-----" + private const val PEM_SIGNATURE_BEGIN = "-----BEGIN SIGNATURE-----" + private const val PEM_SIGNATURE_END = "-----END SIGNATURE-----" + private val IPV4_ADDRESS_REGEX = Regex("""\d{1,3}(\.\d{1,3}){3}""") + } +} diff --git a/app/src/main/java/com/daily/dayo/di/BaseUrlProviderModule.kt b/app/src/main/java/com/daily/dayo/di/BaseUrlProviderModule.kt new file mode 100644 index 000000000..0daf63f20 --- /dev/null +++ b/app/src/main/java/com/daily/dayo/di/BaseUrlProviderModule.kt @@ -0,0 +1,31 @@ +package com.daily.dayo.di + +import android.content.Context +import com.daily.dayo.config.RemoteConfigBaseUrlProvider +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import daily.dayo.domain.provider.BaseUrlProvider +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object BaseUrlProviderModule { + + @Singleton + @Provides + fun provideFirebaseRemoteConfig(): FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance() + + @Singleton + @Provides + fun provideBaseUrlProvider( + @ApplicationContext context: Context, + remoteConfig: FirebaseRemoteConfig + ): BaseUrlProvider = RemoteConfigBaseUrlProvider( + context = context, + remoteConfig = remoteConfig + ) +} diff --git a/data/build.gradle b/data/build.gradle index 4d69cb1a6..585fcdc5c 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -16,6 +16,15 @@ kotlin { Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) +def fallbackBaseUrlProperty = { String fallbackKey, String legacyKey -> + def value = properties.getProperty(fallbackKey) ?: properties.getProperty(legacyKey) + if (!value) { + throw new GradleException("Missing ${fallbackKey} in local.properties") + } + return value +} +def FALLBACK_BASE_URL_DEV = fallbackBaseUrlProperty('FALLBACK_BASE_URL_DEV', 'BASE_URL_DEV') +def FALLBACK_BASE_URL_PROD = fallbackBaseUrlProperty('FALLBACK_BASE_URL_PROD', 'BASE_URL_PROD') android { namespace 'daily.dayo.data' @@ -43,11 +52,11 @@ android { productFlavors { dev { dimension "environment" - buildConfigField("String", "BASE_URL", properties['BASE_URL_DEV']) + buildConfigField("String", "FALLBACK_BASE_URL", FALLBACK_BASE_URL_DEV) } prod { dimension "environment" - buildConfigField("String", "BASE_URL", properties['BASE_URL_PROD']) + buildConfigField("String", "FALLBACK_BASE_URL", FALLBACK_BASE_URL_PROD) } } compileOptions { @@ -91,4 +100,4 @@ dependencies { // paging implementation "androidx.paging:paging-runtime-ktx:$paging_version" -} \ No newline at end of file +} diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/retrofit/interceptor/BaseUrlRewriteInterceptor.kt b/data/src/main/java/daily/dayo/data/datasource/remote/retrofit/interceptor/BaseUrlRewriteInterceptor.kt new file mode 100644 index 000000000..ae7ea4e79 --- /dev/null +++ b/data/src/main/java/daily/dayo/data/datasource/remote/retrofit/interceptor/BaseUrlRewriteInterceptor.kt @@ -0,0 +1,38 @@ +package daily.dayo.data.datasource.remote.retrofit.interceptor + +import daily.dayo.domain.provider.BaseUrlProvider +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BaseUrlRewriteInterceptor @Inject constructor( + private val baseUrlProvider: BaseUrlProvider +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val providerBaseUrl = baseUrlProvider.getBaseUrl() + if (!baseUrlProvider.isTrustedBaseUrl(providerBaseUrl)) return chain.proceed(request) + + val providerUrl = providerBaseUrl.toHttpUrlOrNull() + ?: return chain.proceed(request) + + return try { + val rewrittenUrl = request.url.newBuilder() + .scheme(providerUrl.scheme) + .host(providerUrl.host) + .port(providerUrl.port) + .build() + chain.proceed( + request.newBuilder() + .url(rewrittenUrl) + .build() + ) + } catch (exception: IllegalArgumentException) { + chain.proceed(request) + } + } +} diff --git a/data/src/main/java/daily/dayo/data/di/NetworkModule.kt b/data/src/main/java/daily/dayo/data/di/NetworkModule.kt index 4abf85ccd..bf6c2a0d1 100644 --- a/data/src/main/java/daily/dayo/data/di/NetworkModule.kt +++ b/data/src/main/java/daily/dayo/data/di/NetworkModule.kt @@ -3,12 +3,15 @@ package daily.dayo.data.di import android.content.Context import daily.dayo.data.BuildConfig import daily.dayo.data.datasource.remote.retrofit.factory.NetworkResponseAdapterFactory +import daily.dayo.data.datasource.remote.retrofit.interceptor.BaseUrlRewriteInterceptor import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import daily.dayo.data.datasource.local.SharedManager +import daily.dayo.domain.provider.BaseUrlProvider +import okhttp3.HttpUrl import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -23,18 +26,29 @@ object NetworkModule { @Singleton @Provides - fun provideOkHttpClient(@ApplicationContext context:Context): OkHttpClient { + fun provideOkHttpClient( + @ApplicationContext context: Context, + baseUrlRewriteInterceptor: BaseUrlRewriteInterceptor, + baseUrlProvider: BaseUrlProvider + ): OkHttpClient { val loggingInterceptor = HttpLoggingInterceptor() - loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY) + loggingInterceptor.redactHeader("Authorization") + loggingInterceptor.setLevel( + if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE + ) return OkHttpClient.Builder() + .addInterceptor(baseUrlRewriteInterceptor) .addInterceptor { chain: Interceptor.Chain -> val request = chain.request() + val shouldAttachAuthorization = baseUrlProvider.isTrustedBaseUrl(request.url.toBaseOrigin()) // Header에 AccessToken을 삽입하지 않는 대상 if (request.url.encodedPath.equals("/api/v1/members/kakaoOAuth", true) || request.url.encodedPath.equals("/api/v1/members/signIn", true) || request.url.encodedPath.startsWith("/api/v1/members/signUp", true) ) { chain.proceed(request) + } else if (!shouldAttachAuthorization) { + chain.proceed(request) } else if (request.url.encodedPath.equals("/api/v1/members/refresh", true)) { chain.proceed(request.newBuilder().apply { addHeader( @@ -57,6 +71,13 @@ object NetworkModule { .build() } + private fun HttpUrl.toBaseOrigin(): String = + newBuilder() + .encodedPath("/") + .query(null) + .build() + .toString() + @Singleton @Provides fun provideConverterFactory(): GsonConverterFactory = @@ -69,11 +90,11 @@ object NetworkModule { gsonConverterFactory: GsonConverterFactory ): Retrofit { return Retrofit.Builder() - .baseUrl(BuildConfig.BASE_URL) + .baseUrl(BuildConfig.FALLBACK_BASE_URL) .addCallAdapterFactory(NetworkResponseAdapterFactory()) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(gsonConverterFactory) .client(okHttpClient) .build() } -} \ No newline at end of file +} diff --git a/domain/src/main/java/daily/dayo/domain/provider/BaseUrlProvider.kt b/domain/src/main/java/daily/dayo/domain/provider/BaseUrlProvider.kt new file mode 100644 index 000000000..51ace9930 --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/provider/BaseUrlProvider.kt @@ -0,0 +1,17 @@ +package daily.dayo.domain.provider + +enum class BaseUrlSource { + FALLBACK, + CACHED_LAST_SUCCESS, + REMOTE_CONFIG +} + +interface BaseUrlProvider { + fun getBaseUrl(): String + + fun getBaseUrlSource(): BaseUrlSource + + fun isTrustedBaseUrl(baseUrl: String): Boolean + + suspend fun refreshBaseUrl(): Boolean +} diff --git a/presentation/build.gradle b/presentation/build.gradle index 053d552fc..f79dfa115 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -12,6 +12,15 @@ apply plugin: "org.jetbrains.kotlin.plugin.compose" Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) def NATIVE_APP_KEY = properties.getProperty('NATIVE_APP_KEY') +def fallbackBaseUrlProperty = { String fallbackKey, String legacyKey -> + def value = properties.getProperty(fallbackKey) ?: properties.getProperty(legacyKey) + if (!value) { + throw new GradleException("Missing ${fallbackKey} in local.properties") + } + return value +} +def FALLBACK_BASE_URL_DEV = fallbackBaseUrlProperty('FALLBACK_BASE_URL_DEV', 'BASE_URL_DEV') +def FALLBACK_BASE_URL_PROD = fallbackBaseUrlProperty('FALLBACK_BASE_URL_PROD', 'BASE_URL_PROD') kapt { correctErrorTypes true @@ -57,12 +66,12 @@ android { productFlavors { dev { dimension "environment" - buildConfigField("String", "BASE_URL", properties['BASE_URL_DEV']) + buildConfigField("String", "FALLBACK_BASE_URL", FALLBACK_BASE_URL_DEV) buildConfigField("String", "REWARDED_AD_UNIT_ID_FOLDER", properties['REWARDED_AD_UNIT_ID_FOLDER_DEV']) } prod { dimension "environment" - buildConfigField("String", "BASE_URL", properties['BASE_URL_PROD']) + buildConfigField("String", "FALLBACK_BASE_URL", FALLBACK_BASE_URL_PROD) buildConfigField("String", "REWARDED_AD_UNIT_ID_FOLDER", properties['REWARDED_AD_UNIT_ID_FOLDER_PROD']) } } @@ -240,4 +249,4 @@ dependencies { // Google Ads implementation 'com.google.android.gms:play-services-ads:24.7.0' -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/activity/LoginActivity.kt b/presentation/src/main/java/daily/dayo/presentation/activity/LoginActivity.kt index e7220b946..fd10c5102 100644 --- a/presentation/src/main/java/daily/dayo/presentation/activity/LoginActivity.kt +++ b/presentation/src/main/java/daily/dayo/presentation/activity/LoginActivity.kt @@ -10,22 +10,30 @@ import android.widget.Toast import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.lifecycleScope import com.google.android.play.core.appupdate.AppUpdateManager import com.google.android.play.core.appupdate.AppUpdateManagerFactory import com.google.android.play.core.install.model.UpdateAvailability -import daily.dayo.presentation.viewmodel.AccountViewModel import dagger.hilt.android.AndroidEntryPoint +import daily.dayo.domain.provider.BaseUrlProvider import daily.dayo.presentation.R import daily.dayo.presentation.common.dialog.DefaultDialogAlert +import daily.dayo.presentation.common.url.LocalBaseUrl import daily.dayo.presentation.screen.account.AccountScreen import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.viewmodel.AccountViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint class LoginActivity : AppCompatActivity() { + @Inject + lateinit var baseUrlProvider: BaseUrlProvider + private val loginViewModel by viewModels() private var isReady = false private lateinit var updateDialog: AlertDialog @@ -37,12 +45,25 @@ class LoginActivity : AppCompatActivity() { } super.onCreate(savedInstanceState) createDialogUpdate() - checkUpdate() observeNetworkException() observeApiException() + refreshBaseUrlAndContinue() + } + + private fun refreshBaseUrlAndContinue() { + lifecycleScope.launch { + baseUrlProvider.refreshBaseUrl() + setLoginContent() + checkUpdate() + } + } + + private fun setLoginContent() { setContent { DayoTheme { - AccountScreen() + CompositionLocalProvider(LocalBaseUrl provides baseUrlProvider.getBaseUrl()) { + AccountScreen() + } } } } @@ -135,4 +156,4 @@ class LoginActivity : AppCompatActivity() { } ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt b/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt index 8ab937c2f..44ea04be0 100644 --- a/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt +++ b/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt @@ -13,40 +13,63 @@ import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import com.google.android.gms.ads.AdRequest import com.google.android.gms.ads.LoadAdError import com.google.android.gms.ads.rewarded.RewardedAd import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback import dagger.hilt.android.AndroidEntryPoint +import daily.dayo.domain.provider.BaseUrlProvider import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R +import daily.dayo.presentation.common.url.LocalBaseUrl import daily.dayo.presentation.screen.main.MainScreen import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.viewmodel.AccountViewModel import daily.dayo.presentation.viewmodel.SettingNotificationViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { + @Inject + lateinit var baseUrlProvider: BaseUrlProvider + private val accountViewModel by viewModels() private val settingNotificationViewModel by viewModels() private var rewardedAd: RewardedAd? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) checkCurrentNotification() getNotificationData() askNotificationPermission() loadRewardedAd() + refreshBaseUrlAndSetContent() + } + + private fun refreshBaseUrlAndSetContent() { + lifecycleScope.launch { + baseUrlProvider.refreshBaseUrl() + setMainContent() + } + } + + private fun setMainContent() { setContent { DayoTheme { - MainScreen( - onAdRequest = { onRewardSuccess -> - showAdIfAvailable(onRewardSuccess) - }, - onExit = { finish() } - ) + CompositionLocalProvider(LocalBaseUrl provides baseUrlProvider.getBaseUrl()) { + MainScreen( + onAdRequest = { onRewardSuccess -> + showAdIfAvailable(onRewardSuccess) + }, + onExit = { finish() } + ) + } } } } @@ -176,4 +199,4 @@ class MainActivity : AppCompatActivity() { companion object { val notificationPermission = arrayOf(Manifest.permission.POST_NOTIFICATIONS) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/common/url/BaseUrl.kt b/presentation/src/main/java/daily/dayo/presentation/common/url/BaseUrl.kt new file mode 100644 index 000000000..c3c8d6047 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/common/url/BaseUrl.kt @@ -0,0 +1,27 @@ +package daily.dayo.presentation.common.url + +import androidx.compose.runtime.compositionLocalOf +import daily.dayo.presentation.BuildConfig + +val LocalBaseUrl = compositionLocalOf { BuildConfig.FALLBACK_BASE_URL } + +fun remoteUrl(baseUrl: String, path: String): String { + val normalizedBaseUrl = baseUrl.trimEnd('/') + val normalizedPath = path.trimStart('/') + + return when { + normalizedBaseUrl.isEmpty() -> normalizedPath + normalizedPath.isEmpty() -> normalizedBaseUrl + else -> "$normalizedBaseUrl/$normalizedPath" + } +} + +fun remoteImageUrl(baseUrl: String, imageFileName: String?): String { + val normalizedImageFileName = imageFileName?.trim().orEmpty() + + return if (normalizedImageFileName.isEmpty()) { + "" + } else { + remoteUrl(baseUrl = baseUrl, path = "images/$normalizedImageFileName") + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt index bfbcdddb7..cf2825013 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt @@ -33,9 +33,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.collectAsLazyPagingItems import daily.dayo.domain.model.BookmarkPost -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B import daily.dayo.presentation.theme.Gray2_767B83 @@ -227,10 +228,12 @@ private fun BookmarkPostItem( onBookmarkPostClick: (BookmarkPost) -> Unit, onBookmarkEditClick: () -> Unit ) { + val baseUrl = LocalBaseUrl.current + Box { RoundImageView( context = LocalContext.current, - imageUrl = "${BuildConfig.BASE_URL}/images/${post.thumbnailImage}", + imageUrl = remoteImageUrl(baseUrl, post.thumbnailImage), imageDescription = "bookmark post thumbnail", modifier = Modifier .fillMaxWidth() @@ -258,4 +261,4 @@ private fun BookmarkPostItem( @Composable private fun PreviewBookmarkHeader() { BookmarkHeader(0, 0, true, {}) -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt index 68d617180..7746ffc4f 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt @@ -59,13 +59,14 @@ import daily.dayo.domain.model.FolderInfo import daily.dayo.domain.model.FolderOrder import daily.dayo.domain.model.FolderPost import daily.dayo.domain.model.Privacy -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.dialog.LoadingAlertDialog.createLoadingDialog import daily.dayo.presentation.common.dialog.LoadingAlertDialog.hideLoadingDialog import daily.dayo.presentation.common.dialog.LoadingAlertDialog.resizeDialogFragment import daily.dayo.presentation.common.dialog.LoadingAlertDialog.showLoadingDialog import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B @@ -549,6 +550,7 @@ private fun FolderPostItem( onPostSelect: (Long) -> Unit ) { val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + val baseUrl = LocalBaseUrl.current Box( modifier = Modifier.clickable( @@ -565,7 +567,7 @@ private fun FolderPostItem( ) { RoundImageView( context = LocalContext.current, - imageUrl = "${BuildConfig.BASE_URL}/images/${post.thumbnailImage}", + imageUrl = remoteImageUrl(baseUrl, post.thumbnailImage), imageDescription = "bookmark post thumbnail", modifier = Modifier .fillMaxWidth() diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/FollowScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/FollowScreen.kt index 0d176f589..d30a1ea42 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/FollowScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/FollowScreen.kt @@ -52,9 +52,10 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import daily.dayo.domain.model.Follow import daily.dayo.domain.model.Profile -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B @@ -293,6 +294,8 @@ private fun FollowUserInfo( onProfileClick: (String) -> Unit, onFollowClick: (Follow) -> Unit ) { + val baseUrl = LocalBaseUrl.current + Row( modifier = Modifier .fillMaxWidth() @@ -308,7 +311,7 @@ private fun FollowUserInfo( ) { RoundImageView( context = context, - imageUrl = "${BuildConfig.BASE_URL}/images/${follow.profileImg}", + imageUrl = remoteImageUrl(baseUrl, follow.profileImg), roundSize = 18.dp, modifier = Modifier .size(36.dp) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageEditScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageEditScreen.kt index e1e9aceb1..c321733bf 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageEditScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageEditScreen.kt @@ -58,7 +58,6 @@ import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import daily.dayo.domain.model.Profile -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.Resource import daily.dayo.presentation.common.Status @@ -67,6 +66,8 @@ import daily.dayo.presentation.common.dialog.LoadingAlertDialog.hideLoadingDialo import daily.dayo.presentation.common.dialog.LoadingAlertDialog.resizeDialogFragment import daily.dayo.presentation.common.dialog.LoadingAlertDialog.showLoadingDialog import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B @@ -93,6 +94,7 @@ internal fun MyPageEditScreen( val focusManager = LocalFocusManager.current val alertDialog = remember { mutableStateOf(createLoadingDialog(context)) } val bottomSheetController = LocalBottomSheetController.current + val baseUrl = LocalBaseUrl.current val profileUiState by profileSettingViewModel.profileInfo.observeAsState(Resource.loading(null)) val isNicknameDuplicate by profileSettingViewModel.isNicknameDuplicate.collectAsStateWithLifecycle(false) @@ -132,9 +134,9 @@ internal fun MyPageEditScreen( } } - LaunchedEffect(profileInfo.value?.profileImg, modifiedProfileImage) { + LaunchedEffect(profileInfo.value?.profileImg, modifiedProfileImage, baseUrl) { profileInfo.value?.profileImg?.let { profileImg -> - modifiedProfileImage.value = "${BuildConfig.BASE_URL}/images/${profileImg}" + modifiedProfileImage.value = remoteImageUrl(baseUrl, profileImg) } } @@ -503,4 +505,4 @@ private fun PreviewMyPageEditScreen() { onConfirmClick = {} ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt index 3716b7f37..8d9eda591 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt @@ -46,12 +46,13 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import daily.dayo.domain.model.Folder import daily.dayo.domain.model.Profile -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.Status import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_AD_START_COUNT import daily.dayo.presentation.common.constant.FolderConstants.MAX_FOLDER_COUNT import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B @@ -150,6 +151,8 @@ private fun MyPageProfile( profile: Profile?, onFollowButtonClick: (String, Int) -> Unit ) { + val baseUrl = LocalBaseUrl.current + Row( modifier = Modifier .fillMaxWidth() @@ -160,7 +163,7 @@ private fun MyPageProfile( // profile image RoundImageView( context = LocalContext.current, - imageUrl = "${BuildConfig.BASE_URL}/images/${profile?.profileImg}", + imageUrl = remoteImageUrl(baseUrl, profile?.profileImg), imageDescription = "my page profile image", roundSize = 24.dp, modifier = Modifier diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/notification/NotificationScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/notification/NotificationScreen.kt index c73703b95..a4f0262d3 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/notification/NotificationScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/notification/NotificationScreen.kt @@ -58,9 +58,10 @@ import androidx.paging.compose.collectAsLazyPagingItems import coil.size.Size import daily.dayo.domain.model.Notification import daily.dayo.domain.model.Topic -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.TimeChangerUtil +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray3_9FA5AE @@ -318,6 +319,7 @@ fun NotificationView( onProfileClick: (String) -> Unit = {}, ) { var textLayoutResult by remember { mutableStateOf(null) } + val baseUrl = LocalBaseUrl.current val notificationMessage = buildAnnotatedString { // 1. 닉네임 포함된 메시지 인지 체크 if (!notification.nickname.isNullOrBlank()) { @@ -355,7 +357,7 @@ fun NotificationView( modifier = Modifier.weight(1f), ) { RoundImageView( - imageUrl = "${BuildConfig.BASE_URL}/images/${notification.profileImage}", + imageUrl = remoteImageUrl(baseUrl, notification.profileImage), context = context, modifier = Modifier .size(28.dp) @@ -428,7 +430,7 @@ fun NotificationView( .fillMaxHeight() ) RoundImageView( - imageUrl = "${BuildConfig.BASE_URL}/images/${notification.image!!}", + imageUrl = remoteImageUrl(baseUrl, notification.image!!), context = context, modifier = Modifier .size(56.dp), @@ -482,4 +484,4 @@ fun performNotificationNavigation( } } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/post/PostLikeUsersScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostLikeUsersScreen.kt index db9ebe48c..c9c23ade5 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/post/PostLikeUsersScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostLikeUsersScreen.kt @@ -46,9 +46,10 @@ import androidx.paging.compose.itemKey import coil.compose.AsyncImage import coil.request.ImageRequest import daily.dayo.domain.model.LikeUser -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray2_767B83 @@ -197,6 +198,8 @@ private fun LikeUserItem( onProfileClick: (String) -> Unit, onFollowClick: (LikeUser) -> Unit ) { + val baseUrl = LocalBaseUrl.current + Surface( color = White_FFFFFF, modifier = Modifier @@ -211,7 +214,7 @@ private fun LikeUserItem( // profile AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data("${BuildConfig.BASE_URL}/images/${likeUser.profileImg}") + .data(remoteImageUrl(baseUrl, likeUser.profileImg)) .build(), contentDescription = "${likeUser.nickname} + profile", contentScale = ContentScale.Crop, @@ -272,4 +275,4 @@ private fun PreviewPostLikeUsersScreen() { onFollowClick = { }, onBackClick = { } ) -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt index 60df1ef55..a81e4a597 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt @@ -49,10 +49,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import daily.dayo.domain.model.Folder import daily.dayo.domain.model.Profile -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.Status import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B @@ -276,6 +277,8 @@ private fun UserProfile( profile: Profile, onFollowMenuClick: (String, Int) -> Unit ) { + val baseUrl = LocalBaseUrl.current + Row( modifier = Modifier .fillMaxWidth() @@ -286,7 +289,7 @@ private fun UserProfile( // profile image RoundImageView( context = LocalContext.current, - imageUrl = "${BuildConfig.BASE_URL}/images/${profile.profileImg}", + imageUrl = remoteImageUrl(baseUrl, profile.profileImg), imageDescription = "profile image", roundSize = 24.dp, modifier = Modifier @@ -484,4 +487,3 @@ private val DEFAULT_PROFILE = Profile( followingCount = 10, follow = null, ) - diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/rules/RuleScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/rules/RuleScreen.kt index 292e35a7e..153050278 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/rules/RuleScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/rules/RuleScreen.kt @@ -15,8 +15,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.viewinterop.AndroidView -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteUrl import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.view.NoRippleIconButton import daily.dayo.presentation.view.TopNavigation @@ -36,6 +37,8 @@ fun RuleScreen( onBackClick: () -> Unit = {}, ruleType: RuleType = RuleType.PRIVACY_POLICY ) { + val baseUrl = LocalBaseUrl.current + Surface( modifier = Modifier .background(DayoTheme.colorScheme.background) @@ -59,7 +62,7 @@ fun RuleScreen( webViewClient = WebViewClient() settings.javaScriptEnabled = false overScrollMode = View.OVER_SCROLL_NEVER - loadUrl("${BuildConfig.BASE_URL}/${ruleType.fileName}.html") + loadUrl(remoteUrl(baseUrl, "${ruleType.fileName}.html")) setOnKeyListener( View.OnKeyListener { v, keyCode, event -> @@ -94,4 +97,4 @@ fun RuleActionbarLayout( title = ruleType.koreanName, titleAlignment = TopNavigationAlign.CENTER ) -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchPostHashtagScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchPostHashtagScreen.kt index 3db82539f..0d42e0a02 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchPostHashtagScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchPostHashtagScreen.kt @@ -32,9 +32,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemKey import daily.dayo.domain.model.SearchOrder -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B import daily.dayo.presentation.theme.Gray2_767B83 @@ -54,6 +55,7 @@ fun SearchPostHashtagScreen( val searchHashtagOrder by searchViewModel.searchHashtagOrder.collectAsStateWithLifecycle() val hashtagPosts = searchViewModel.searchTagList.collectAsLazyPagingItems() val hashtagPostsCount by searchViewModel.searchTagTotalCount.collectAsStateWithLifecycle(0) + val baseUrl = LocalBaseUrl.current LaunchedEffect(Unit) { with(searchViewModel) { @@ -110,7 +112,7 @@ fun SearchPostHashtagScreen( item.let { post -> RoundImageView( context = LocalContext.current, - imageUrl = "${BuildConfig.BASE_URL}/images/${post?.thumbnailImage}", + imageUrl = remoteImageUrl(baseUrl, post?.thumbnailImage), imageDescription = "searched Image", modifier = Modifier .fillMaxWidth() @@ -175,4 +177,4 @@ private fun SearchResultDescription( ) } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt index 24940a43f..0773fe2dc 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt @@ -77,10 +77,11 @@ import com.skydoves.landscapist.glide.GlideImage import daily.dayo.domain.model.Search import daily.dayo.domain.model.SearchHistoryType import daily.dayo.domain.model.SearchUser -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.Event import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B import daily.dayo.presentation.theme.Gray2_767B83 @@ -393,6 +394,7 @@ fun SearchResultTagView( onPostClick: (Long) -> Unit ) { val imageInteractionSource = remember { MutableInteractionSource() } + val baseUrl = LocalBaseUrl.current Box( modifier = Modifier .fillMaxSize() @@ -414,7 +416,7 @@ fun SearchResultTagView( item?.let { post -> RoundImageView( context = LocalContext.current, - imageUrl = "${BuildConfig.BASE_URL}/images/${post.thumbnailImage}", + imageUrl = remoteImageUrl(baseUrl, post.thumbnailImage), imageDescription = "searched Image", modifier = Modifier .matchParentSize() @@ -523,8 +525,9 @@ fun SearchResultUserView( @Composable private fun SearchResultUserImageLayout(user: SearchUser, onClickProfile: (String) -> Unit) { val imageInteractionSource = remember { MutableInteractionSource() } + val baseUrl = LocalBaseUrl.current GlideImage( - imageModel = { "${BuildConfig.BASE_URL}/images/${user.profileImg}" }, + imageModel = { remoteImageUrl(baseUrl, user.profileImg) }, imageOptions = ImageOptions( contentDescription = "image description", contentScale = ContentScale.Crop, diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt index 465f48788..94ec584a6 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt @@ -39,9 +39,10 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.size.Size -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray3_9FA5AE @@ -227,6 +228,8 @@ fun BlockedUser( onUnblockClick: (String) -> Unit = {}, context: Context = LocalContext.current, ) { + val baseUrl = LocalBaseUrl.current + Row( modifier = Modifier .background(DayoTheme.colorScheme.background) @@ -237,7 +240,7 @@ fun BlockedUser( verticalAlignment = Alignment.CenterVertically, ) { RoundImageView( - imageUrl = "${BuildConfig.BASE_URL}/images/${imageFileName}", + imageUrl = remoteImageUrl(baseUrl, imageFileName), context = context, modifier = Modifier .size(36.dp) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt index 6dee5b312..8d0532e7e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt @@ -49,11 +49,12 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import daily.dayo.domain.model.Profile -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.activity.LoginActivity import daily.dayo.presentation.activity.MainActivity import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B @@ -256,6 +257,8 @@ private fun SettingProfile( profile: Profile?, onProfileEditClick: () -> Unit ) { + val baseUrl = LocalBaseUrl.current + Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally @@ -263,7 +266,7 @@ private fun SettingProfile( // profile image RoundImageView( context = LocalContext.current, - imageUrl = "${BuildConfig.BASE_URL}/images/${profile?.profileImg}", + imageUrl = remoteImageUrl(baseUrl, profile?.profileImg), imageDescription = stringResource(id = R.string.setting_my_profile_image_description), roundSize = 28.dp, modifier = Modifier.size(56.dp) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt index f72d9b22b..42712909f 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt @@ -41,7 +41,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.size.Size import daily.dayo.domain.model.Folder import daily.dayo.domain.model.Privacy -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_AD_START_COUNT import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_THUMBNAIL_RADIUS_SIZE @@ -49,6 +48,8 @@ import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_THUMBNAIL_ import daily.dayo.presentation.common.constant.FolderConstants.MAX_FOLDER_COUNT import daily.dayo.presentation.common.extension.clickableSingle import daily.dayo.presentation.common.extension.limitTo +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B @@ -265,9 +266,10 @@ fun WriteFolderItemLayout( isSelected: Boolean = true, onFolderClick: (Long, String) -> Unit = { _, _ -> }, ) { + val baseUrl = LocalBaseUrl.current val thumbnailModel: Any = folder.thumbnailImage .takeIf { it.isNotBlank() } - ?.let { "${BuildConfig.BASE_URL}/images/$it" } + ?.let { remoteImageUrl(baseUrl, it) } ?: R.drawable.img_default_folder_dayo_logo Row( diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt index ce30ca565..eb01bd271 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt @@ -73,10 +73,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import coil.request.ImageRequest import daily.dayo.domain.model.Category -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.Status import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.screen.home.CategoryMenu import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme @@ -403,6 +404,8 @@ fun WriteUploadImages( deleteImage: (Int) -> Unit, onEditImage: (Int) -> Unit, ) { + val baseUrl = LocalBaseUrl.current + LazyRow( contentPadding = PaddingValues(start = 18.dp, end = 18.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -419,7 +422,7 @@ fun WriteUploadImages( val imageRequest = ImageRequest.Builder(context) .data( if (isPostEditMode) { - "${BuildConfig.BASE_URL}/images/${imageAsset.uriString}" + remoteImageUrl(baseUrl, imageAsset.uriString) } else { imageAsset.uriString } diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt index ee72340c7..0cf7ed487 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt @@ -58,10 +58,11 @@ import daily.dayo.domain.model.Comment import daily.dayo.domain.model.Comments import daily.dayo.domain.model.MentionUser import daily.dayo.domain.model.SearchUser -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.TimeChangerUtil import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B @@ -187,12 +188,14 @@ fun CommentView( onClickReport: (Long) -> Unit, modifier: Modifier ) { + val baseUrl = LocalBaseUrl.current + Column( modifier = modifier, ) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { RoundImageView( - imageUrl = "${BuildConfig.BASE_URL}/images/${comment.profileImg}", + imageUrl = remoteImageUrl(baseUrl, comment.profileImg), context = LocalContext.current, modifier = Modifier .clip(CircleShape) @@ -322,6 +325,7 @@ fun getAnnotatedCommentContent(content: String, mentionList: List): @Composable fun CommentMentionSearchView(userResults: LazyPagingItems, onClickFollowUser: (SearchUser) -> Unit) { val placeholder = AppCompatResources.getDrawable(LocalContext.current, R.drawable.ic_profile_default) + val baseUrl = LocalBaseUrl.current LazyColumn( modifier = Modifier .background(DayoTheme.colorScheme.background) @@ -347,7 +351,7 @@ fun CommentMentionSearchView(userResults: LazyPagingItems, onClickFo verticalAlignment = Alignment.CenterVertically ) { RoundImageView( - imageUrl = "${BuildConfig.BASE_URL}/images/${user.profileImg}", + imageUrl = remoteImageUrl(baseUrl, user.profileImg), context = LocalContext.current, modifier = Modifier .clip(CircleShape) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/DetailPostView.kt b/presentation/src/main/java/daily/dayo/presentation/view/DetailPostView.kt index a95193d91..70832abe4 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/DetailPostView.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/DetailPostView.kt @@ -46,10 +46,11 @@ import coil.request.ImageRequest import daily.dayo.domain.model.Category import daily.dayo.domain.model.PostDetail import daily.dayo.domain.model.categoryKR -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.TimeChangerUtil import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray1_50545B @@ -87,6 +88,7 @@ fun DetailPostView( var showDialog by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() val context = LocalContext.current + val baseUrl = LocalBaseUrl.current Column(modifier = modifier) { // publisher info @@ -100,7 +102,7 @@ fun DetailPostView( // user profile image AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data("${BuildConfig.BASE_URL}/images/${post.profileImg}") + .data(remoteImageUrl(baseUrl, post.profileImg)) .build(), contentDescription = "${post.nickname} profile", contentScale = ContentScale.Crop, @@ -179,7 +181,7 @@ fun DetailPostView( HorizontalPager(state = pagerState) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data("${BuildConfig.BASE_URL}/images/${postImages[it]}") + .data(remoteImageUrl(baseUrl, postImages[it])) .build(), contentDescription = "post images", contentScale = ContentScale.Crop, diff --git a/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt b/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt index 6c9f2e4b5..4de087aae 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt @@ -61,10 +61,11 @@ import coil.request.ImageRequest import daily.dayo.domain.model.Category import daily.dayo.domain.model.Post import daily.dayo.domain.model.categoryKR -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.TimeChangerUtil import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray2_767B83 @@ -98,6 +99,7 @@ fun FeedPostView( var showDialog by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() val context = LocalContext.current + val baseUrl = LocalBaseUrl.current Column(modifier = modifier) { // publisher info @@ -111,7 +113,7 @@ fun FeedPostView( // user profile image AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data("${BuildConfig.BASE_URL}/images/${post.userProfileImage}") + .data(remoteImageUrl(baseUrl, post.userProfileImage)) .build(), contentDescription = "${post.nickname} + profile", contentScale = ContentScale.Crop, @@ -179,7 +181,7 @@ fun FeedPostView( HorizontalPager(state = pagerState) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data("${BuildConfig.BASE_URL}/images/${postImages[it]}") + .data(remoteImageUrl(baseUrl, postImages[it])) .build(), contentDescription = "post images", contentScale = ContentScale.Crop, diff --git a/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt b/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt index 6727d234a..192590316 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt @@ -24,9 +24,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import daily.dayo.domain.model.Folder import daily.dayo.domain.model.Privacy -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Gray3_9FA5AE @@ -40,6 +41,7 @@ fun FolderView( modifier: Modifier = Modifier, ) { val imageInteractionSource = remember { MutableInteractionSource() } + val baseUrl = LocalBaseUrl.current Column(modifier = modifier .clickableSingle( interactionSource = imageInteractionSource, @@ -55,7 +57,7 @@ fun FolderView( // thumbnail image RoundImageView( context = LocalContext.current, - imageUrl = "${BuildConfig.BASE_URL}/images/${folder.thumbnailImage}", + imageUrl = remoteImageUrl(baseUrl, folder.thumbnailImage), imageDescription = folder.title, modifier = Modifier .border(BorderStroke(1.dp, Gray5_E8EAEE), RoundedCornerShape(8.dp)) @@ -92,4 +94,4 @@ fun FolderView( ) } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/HomePostView.kt b/presentation/src/main/java/daily/dayo/presentation/view/HomePostView.kt index e30856e9d..acd6c7114 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/HomePostView.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/HomePostView.kt @@ -31,8 +31,9 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import daily.dayo.domain.model.Post -import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R +import daily.dayo.presentation.common.url.LocalBaseUrl +import daily.dayo.presentation.common.url.remoteImageUrl import daily.dayo.presentation.common.extension.clickableSingle import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme @@ -49,6 +50,7 @@ fun HomePostView( onClickProfile: () -> Unit ) { val imageInteractionSource = remember { MutableInteractionSource() } + val baseUrl = LocalBaseUrl.current Column(modifier = modifier) { Box( modifier = Modifier @@ -58,7 +60,7 @@ fun HomePostView( // thumbnail image RoundImageView( context = LocalContext.current, - imageUrl = "${BuildConfig.BASE_URL}/images/${post.thumbnailImage}", + imageUrl = remoteImageUrl(baseUrl, post.thumbnailImage), imageDescription = "dayo pick image", modifier = Modifier .matchParentSize() @@ -110,7 +112,7 @@ fun HomePostView( ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data("${BuildConfig.BASE_URL}/images/${post.userProfileImage}") + .data(remoteImageUrl(baseUrl, post.userProfileImage)) .build(), contentDescription = "${post.nickname} + profile", contentScale = ContentScale.Crop,