Skip to content
Open
1 change: 0 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<!-- Android 12(API 32) 이하에서 이미지를 읽기 위한 권한 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />


<application
android:name=".presentation.KustaurantApplication"
android:allowBackup="true"
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/kust/kustaurant/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.kust.kustaurant
import android.os.Bundle
import android.widget.Toast
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import com.kust.kustaurant.databinding.ActivityMainBinding
import com.kust.kustaurant.presentation.common.BaseActivity
import com.kust.kustaurant.presentation.ui.community.CommunityFragment
import com.kust.kustaurant.presentation.ui.draw.DrawFragment
import com.kust.kustaurant.presentation.ui.home.HomeFragment
Expand All @@ -13,7 +13,7 @@ import com.kust.kustaurant.presentation.ui.tier.TierFragment
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
class MainActivity : BaseActivity() {
lateinit var binding: ActivityMainBinding
private var TIME_INTERVAL: Long = 2000
private var backPressedTime: Long = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,19 @@ class NotFoundException(
): RuntimeException()

/**
* 500: server error
* 500: server error, except 503
*/
class ServerException(
override val message: String?
): RuntimeException()

/**
* 503: server error
*/
class ServerException503(
override val message: String?
): RuntimeException()

/**
* response time out
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ object NetworkUtils {
}

// retrofit 통신 코드를 매핑
private fun mapHttpException(e: HttpException): RuntimeException {
fun mapHttpException(e: HttpException): RuntimeException {
return when(e.code()) {
400 -> BadRequestException(message = e.message())
401 -> UnauthorizedException(message = e.message())
403 -> ForbiddenException(message = e.message())
404 -> NotFoundException(message = e.message())
500, 501, 502, 503 -> ServerException(message = e.message())
503 -> ServerException503(message = e.message())
500, 501, 502 -> ServerException(message = e.message())
else -> OtherHttpException(code = e.code(), message = e.message())
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.kust.kustaurant.data.network

import com.kust.kustaurant.domain.common.notice.ServiceDownNotifier
import com.kust.kustaurant.domain.model.ServiceDownEvent
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject

class ServiceUnavailableNotifyInterceptor @Inject constructor(
private val notifier: ServiceDownNotifier
) : Interceptor {
private val seq = java.util.concurrent.atomic.AtomicLong(0)

override fun intercept(chain: Interceptor.Chain): Response {
val resp = chain.proceed(chain.request())

if (resp.code == 503) {
val bodyStr = runCatching { resp.peekBody(64 * 1024).string() }.getOrNull()
val (code, message) = parsePayload(bodyStr)

if (code.equals("MAINTENANCE", ignoreCase = true)) {
notifier.notify503(
ServiceDownEvent(
seq = seq.incrementAndGet(),
code = code,
message = message ?: "현재 앱 업데이트 중입니다. 웹 서비스는 정상적으로 이용 가능합니다. 빠른 시일 내에 찾아뵙겠습니다."
)
)
}
}
return resp
}

private fun parsePayload(raw: String?): Pair<String?, String?> {
if (raw.isNullOrBlank()) return null to null
return runCatching {
val j = org.json.JSONObject(raw)
j.optString("code", null) to j.optString("message", null)
}.getOrDefault(null to null)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.kust.kustaurant.data.network.bus

import com.kust.kustaurant.domain.common.notice.ServiceDownNotifier
import com.kust.kustaurant.domain.model.ServiceDownEvent
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow

@Singleton
class ServiceDownBus @Inject constructor() : ServiceDownNotifier {
private val seq = java.util.concurrent.atomic.AtomicLong(0)
private val _events = MutableSharedFlow<ServiceDownEvent>(
replay = 1,
extraBufferCapacity = 0
)
override val events: SharedFlow<ServiceDownEvent> = _events
override fun notify503(event: ServiceDownEvent) {
_events.tryEmit(event.copy(seq = seq.incrementAndGet()))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class CommunityRepositoryImpl @Inject constructor(
private val communityApi : CommunityApi
) : CommunityRepository {
override suspend fun getPostListData(postCategory: String, page: Int, sort: String): List<CommunityPost> {
return communityApi.getCommunityPostListData(postCategory, page, sort)
return communityApi
.getCommunityPostListData(postCategory, page, sort)
}

override suspend fun getRankingListData(
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/kust/kustaurant/di/AppModule.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.kust.kustaurant.di

import android.content.Context
import com.kust.kustaurant.presentation.ui.community.ImageUtil
import com.kust.kustaurant.presentation.ui.util.ImageUtil
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.kust.kustaurant.di.network

import android.content.Context
import com.kust.kustaurant.BuildConfig
import com.kust.kustaurant.data.network.ServiceUnavailableNotifyInterceptor
import com.kust.kustaurant.data.network.TokenAuthenticator
import com.kust.kustaurant.data.network.XAccessTokenInterceptor
import com.kust.kustaurant.data.remote.CommunityApi
Expand Down Expand Up @@ -40,20 +41,24 @@ object NetworkModule {
@Singleton
fun provideOkHttpClient(
loggingInterceptor: HttpLoggingInterceptor,
@ApplicationContext context: Context
@ApplicationContext context: Context,
notify503: ServiceUnavailableNotifyInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.readTimeout(30000, TimeUnit.MILLISECONDS)
.connectTimeout(30000, TimeUnit.MILLISECONDS)
.addInterceptor(notify503)
.addInterceptor(loggingInterceptor)
.addInterceptor(XAccessTokenInterceptor(context))
.authenticator(TokenAuthenticator(context)) // Authenticator 추가
.authenticator(TokenAuthenticator(context))
.build()
}

@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
fun provideRetrofit(
okHttpClient: OkHttpClient,
): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
Expand Down Expand Up @@ -115,5 +120,4 @@ object NetworkModule {
fun provideSearchApi(retrofit: Retrofit): SearchApi {
return retrofit.create(SearchApi::class.java)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.kust.kustaurant.di.network

import com.kust.kustaurant.data.network.bus.ServiceDownBus
import com.kust.kustaurant.domain.common.notice.ServiceDownNotifier
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class NotifierBindingsModule {

@Binds
@Singleton
abstract fun bindServiceDownNotifier(
impl: ServiceDownBus
): ServiceDownNotifier
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.kust.kustaurant.domain.common.notice

import com.kust.kustaurant.domain.model.ServiceDownEvent
import kotlinx.coroutines.flow.SharedFlow

interface ServiceDownNotifier {
val events: SharedFlow<ServiceDownEvent>
fun notify503(event: ServiceDownEvent)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.kust.kustaurant.domain.model

data class ServiceDownEvent(
val seq: Long,
val code: String?,
val message: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.kust.kustaurant.presentation.common

import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kust.kustaurant.data.network.bus.ServiceDownBus
import com.kust.kustaurant.presentation.common.notify503.GlobalServiceDownDialog
import com.kust.kustaurant.presentation.common.notify503.ServiceDownDialogStateViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject

const val DIALOG_TAG = "ServiceDownDialog"

@AndroidEntryPoint
abstract class BaseActivity : AppCompatActivity(), GlobalServiceDownDialog.Listener {
@Inject lateinit var serviceDownBus: ServiceDownBus
private val stateVM: ServiceDownDialogStateViewModel by viewModels()
private var serviceDialogShowing = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
serviceDownBus.events.collect { event ->
if (!shouldHandleGlobal503() || isFinishing || isDestroyed) return@collect
if (!stateVM.shouldHandle(event.seq)) return@collect

val msg = event.message ?: "현재 서비스 이용이 원활하지 않습니다.\n잠시 후 다시 시도해 주세요."
val fm = supportFragmentManager
val existing = fm.findFragmentByTag(DIALOG_TAG) as? GlobalServiceDownDialog
if (existing?.isAdded == true && existing.isVisible) {
existing.updateMessage(msg)
} else {
existing?.dismissAllowingStateLoss()
fm.executePendingTransactions()
GlobalServiceDownDialog.newInstance(msg).show(fm, DIALOG_TAG)
}
}
}
}
}

override fun onServiceDownDialogDismissed() {
serviceDialogShowing = false
}

protected open fun shouldHandleGlobal503(): Boolean = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.kust.kustaurant.presentation.common.notify503

import android.app.Dialog
import android.content.DialogInterface
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import com.kust.kustaurant.databinding.DialogShowServiceUpdateBinding

class GlobalServiceDownDialog : DialogFragment() {
interface Listener {
fun onServiceDownDialogDismissed()
}

private var _binding: DialogShowServiceUpdateBinding? = null
private val binding get() = _binding!!

companion object {
private const val ARG_MESSAGE = "arg_message"

fun newInstance(message: String) =
GlobalServiceDownDialog().apply {
arguments = bundleOf(ARG_MESSAGE to message)
}
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireContext())
_binding = DialogShowServiceUpdateBinding.inflate(layoutInflater)
dialog.setContentView(binding.root)
dialog.setCancelable(true)
dialog.window?.setLayout(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))

val message = requireArguments().getString(ARG_MESSAGE).orEmpty()

binding.notifyClRoot.clipToOutline = true
binding.notifyTvContent.text = message
binding.notifyTvConfirm.setOnClickListener { dismiss() }

return dialog
}

fun updateMessage(msg: String) {
binding.notifyTvContent.text = msg
}

override fun onDestroyView() {
_binding = null
super.onDestroyView()
}

override fun onDismiss(dialog: DialogInterface) {
(activity as? Listener)?.onServiceDownDialogDismissed()
super.onDismiss(dialog)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.kust.kustaurant.presentation.common.notify503

import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class ServiceDownDialogStateViewModel @Inject constructor() : ViewModel() {
private var lastHandledSeq: Long = 0L
fun shouldHandle(seq: Long): Boolean {
if (seq == lastHandledSeq) return false
lastHandledSeq = seq
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import android.widget.LinearLayout
import android.widget.PopupWindow
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.google.android.material.bottomsheet.BottomSheetDialog
Expand All @@ -25,13 +24,14 @@ import com.kust.kustaurant.data.getAccessToken
import com.kust.kustaurant.databinding.ActivityCommunityPostDetailBinding
import com.kust.kustaurant.databinding.BottomSheetCommentBinding
import com.kust.kustaurant.databinding.PopupCommuPostDetailDotsBinding
import com.kust.kustaurant.presentation.common.BaseActivity
import com.kust.kustaurant.presentation.model.CommunityPostIntent
import com.kust.kustaurant.presentation.model.UiState
import com.kust.kustaurant.presentation.ui.splash.StartActivity
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class CommunityPostDetailActivity : AppCompatActivity() {
class CommunityPostDetailActivity : BaseActivity() {
private var _binding: ActivityCommunityPostDetailBinding? = null
private val binding get() = _binding!!
private val viewModel: CommunityPostDetailViewModel by viewModels()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class CommunityPostDetailViewModel @Inject constructor(
private val _uiState = MutableLiveData<UiState>(UiState.Idle)
val uiState: LiveData<UiState> = _uiState

var lastHandledSeq: Long? = null
fun loadCommunityPostDetail(postId: Int) {
viewModelScope.launch {
try {
Expand Down
Loading