diff --git a/data/src/main/java/com/example/data/hilt/module/NetworkModule.kt b/data/src/main/java/com/example/data/hilt/module/NetworkModule.kt index 3b981d59..9bc11c7e 100644 --- a/data/src/main/java/com/example/data/hilt/module/NetworkModule.kt +++ b/data/src/main/java/com/example/data/hilt/module/NetworkModule.kt @@ -22,9 +22,10 @@ import javax.inject.Singleton object NetworkModule { private const val BASE_URL = "http://113.198.236.222:8080" - private const val MOCK_BASE_URL = "https://21398c26-cbfe-4b79-8276-abe5ff68e880.mock.pstmn.io" - // "https://40f7f957-47a1-488e-ac1e-c8a882e2119d.mock.pstmn.io" + private const val MOCK_BASE_URL = "https://980e509b-75c9-4e14-96a9-2691dedc1237.mock.pstmn.io" + // "https://21398c26-cbfe-4b79-8276-abe5ff68e880.mock.pstmn.io" // "https://980e509b-75c9-4e14-96a9-2691dedc1237.mock.pstmn.io" + // "https://40f7f957-47a1-488e-ac1e-c8a882e2119d.mock.pstmn.io" @Singleton @Provides @@ -65,9 +66,9 @@ object NetworkModule { @Provides fun provideOkHttp(requestInterceptor: RequestInterceptor): OkHttpClient { return OkHttpClient.Builder().apply { - connectTimeout(10, TimeUnit.SECONDS) - readTimeout(10, TimeUnit.SECONDS) - writeTimeout(10, TimeUnit.SECONDS) + connectTimeout(30, TimeUnit.SECONDS) + readTimeout(30, TimeUnit.SECONDS) + writeTimeout(30, TimeUnit.SECONDS) addInterceptor(requestInterceptor) addInterceptor( // Http 요청/응답 중 Body만 로깅 HttpLoggingInterceptor(ApiLogger()) diff --git a/data/src/main/java/com/example/data/toothbrush/ToothBrushModule.kt b/data/src/main/java/com/example/data/toothbrush/ToothBrushModule.kt new file mode 100644 index 00000000..3a8cbf70 --- /dev/null +++ b/data/src/main/java/com/example/data/toothbrush/ToothBrushModule.kt @@ -0,0 +1,34 @@ +package com.example.data.toothbrush + +import com.example.data.hilt.module.NetworkModule +import com.example.data.hilt.qualifier.BaseRetrofit +import com.example.data.toothbrush.remote.api.ToothBrushApi +import com.example.data.toothbrush.repository.ToothBrushRepositoryImpl +import com.example.domain.toothbrush.ToothBrushRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module(includes = [NetworkModule::class]) +@InstallIn(SingletonComponent::class) +internal class ToothBrushModule { + + @Singleton + @Provides + fun provideToothBrushApi( + @BaseRetrofit retrofit: Retrofit + ): ToothBrushApi { + return retrofit.create(ToothBrushApi::class.java) + } + + @Singleton + @Provides + fun provideToothBrushRepository( + toothBrushApi: ToothBrushApi + ): ToothBrushRepository { + return ToothBrushRepositoryImpl(toothBrushApi) + } +} \ No newline at end of file diff --git a/data/src/main/java/com/example/data/toothbrush/remote/api/ToothBrushApi.kt b/data/src/main/java/com/example/data/toothbrush/remote/api/ToothBrushApi.kt new file mode 100644 index 00000000..572f63a9 --- /dev/null +++ b/data/src/main/java/com/example/data/toothbrush/remote/api/ToothBrushApi.kt @@ -0,0 +1,19 @@ +package com.example.data.toothbrush.remote.api + +import com.example.data.toothbrush.remote.response.ToothBrushResponse +import okhttp3.MultipartBody +import retrofit2.Response +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.Path + +internal interface ToothBrushApi { + + @Multipart + @POST("ToothBrushStatus/{userId}") + suspend fun checkToothBrushStatus( + @Path("userId") userId: String, + @Part imageFile: MultipartBody.Part + ): Response +} \ No newline at end of file diff --git a/data/src/main/java/com/example/data/toothbrush/remote/response/ToothBrushResponse.kt b/data/src/main/java/com/example/data/toothbrush/remote/response/ToothBrushResponse.kt new file mode 100644 index 00000000..f9d0d67e --- /dev/null +++ b/data/src/main/java/com/example/data/toothbrush/remote/response/ToothBrushResponse.kt @@ -0,0 +1,21 @@ +package com.example.data.toothbrush.remote.response + +import android.util.Base64 +import com.example.data.common.mapper.DataMapper +import com.example.data.common.network.BaseResponse +import com.example.domain.toothbrush.model.ToothBrush +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +class ToothBrushResponse( + @SerializedName("image") val image: String +): BaseResponse { + companion object: DataMapper { + override fun ToothBrushResponse.toDomainModel(): ToothBrush { + return ToothBrush( + Base64.decode(image, Base64.DEFAULT) + ) + } + } +} \ No newline at end of file diff --git a/data/src/main/java/com/example/data/toothbrush/repository/ToothBrushRepositoryImpl.kt b/data/src/main/java/com/example/data/toothbrush/repository/ToothBrushRepositoryImpl.kt new file mode 100644 index 00000000..e7d56b35 --- /dev/null +++ b/data/src/main/java/com/example/data/toothbrush/repository/ToothBrushRepositoryImpl.kt @@ -0,0 +1,47 @@ +package com.example.data.toothbrush.repository + +import android.graphics.Bitmap +import com.example.data.common.network.ApiResponse +import com.example.data.common.network.ApiResponseHandler +import com.example.data.common.network.ErrorResponse.Companion.toDomainModel +import com.example.data.toothbrush.remote.api.ToothBrushApi +import com.example.data.toothbrush.remote.response.ToothBrushResponse.Companion.toDomainModel +import com.example.domain.common.base.ResponseState +import com.example.domain.toothbrush.ToothBrushRepository +import com.example.domain.toothbrush.model.ToothBrush +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import javax.inject.Inject + +internal class ToothBrushRepositoryImpl @Inject constructor( + private val toothBrushApi: ToothBrushApi +): ToothBrushRepository { + + override suspend fun checkToothBrushStatus( + userId: String, + image: ByteArray + ): Flow> { + val requestBody = image.toRequestBody("image/png".toMediaTypeOrNull()) + val part = MultipartBody.Part.createFormData("imageFile", "image.png", requestBody) + + return flow { + ApiResponseHandler().handle { + toothBrushApi.checkToothBrushStatus(userId, part) + }.onEach { result -> + when(result){ + is ApiResponse.Success -> { + emit(ResponseState.Success(result.data.toDomainModel())) + } + is ApiResponse.Error -> { + emit(ResponseState.Error(result.error.toDomainModel())) + } + } + }.collect() + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/example/domain/assessment/model/Assessment.kt b/domain/src/main/java/com/example/domain/assessment/model/Assessment.kt new file mode 100644 index 00000000..df622cb7 --- /dev/null +++ b/domain/src/main/java/com/example/domain/assessment/model/Assessment.kt @@ -0,0 +1,61 @@ +package com.example.domain.assessment.model + +import androidx.annotation.DrawableRes +import com.example.domain.common.base.BaseModel +import kotlinx.parcelize.Parcelize + +@Parcelize +sealed class Assessment : BaseModel { + + class Header ( + val resultTag: String, + val title: String, + val explain: String, + ): Assessment() + + class Stats ( + val facialAsymmetry: Int, + val eyeDegree: Int, + val lipDegree: Int, + ): Assessment() + + class FaqList(val faqList: ArrayList) : Assessment() { + + constructor(question: Array, answer: Array): this(arrayListOf()) { + for(i in question.indices){ + this.faqList.add( + Faq( + question = question[i], + answer = answer[i] + ) + ) + } + } + + @Parcelize + data class Faq( + val question: String, + val answer: String, + var isExpanded: Boolean = false + ) : BaseModel + } + + class RecommendVideoList( + val videos: ArrayList + ): Assessment() { + + /** + * Thumbnail(ByteArray, Resource) + * 두 가지 중 하나의 타입으로 전달 받을 수 있음 + */ + @Parcelize + data class RecommendVideo( + val title: String, + val youtubeUrl: String, + @DrawableRes val thumbnailRes: Int? = null, + val thumbnail: ByteArray? = null, + ) : BaseModel + } +} + + diff --git a/domain/src/main/java/com/example/domain/banner/Banner.kt b/domain/src/main/java/com/example/domain/banner/Banner.kt new file mode 100644 index 00000000..6ee9fa4a --- /dev/null +++ b/domain/src/main/java/com/example/domain/banner/Banner.kt @@ -0,0 +1,18 @@ +package com.example.domain.banner + +import com.example.domain.common.base.BaseModel +import kotlinx.parcelize.Parcelize + +enum class BannerType { + BANNER_MEDICINE, + BANNER_AI, + BANNER_CHAT_BOT, + BANNER_NEAR_BY_HOSPITAL +} + +@Parcelize +class Banner( + val id: Int, + val imageRes: Int, + val type: BannerType +): BaseModel \ No newline at end of file diff --git a/domain/src/main/java/com/example/domain/toothbrush/ToothBrushRepository.kt b/domain/src/main/java/com/example/domain/toothbrush/ToothBrushRepository.kt new file mode 100644 index 00000000..ecc8e858 --- /dev/null +++ b/domain/src/main/java/com/example/domain/toothbrush/ToothBrushRepository.kt @@ -0,0 +1,9 @@ +package com.example.domain.toothbrush + +import com.example.domain.common.base.ResponseState +import com.example.domain.toothbrush.model.ToothBrush +import kotlinx.coroutines.flow.Flow + +interface ToothBrushRepository { + suspend fun checkToothBrushStatus(userId: String, image:ByteArray): Flow> +} \ No newline at end of file diff --git a/domain/src/main/java/com/example/domain/toothbrush/model/ToothBrush.kt b/domain/src/main/java/com/example/domain/toothbrush/model/ToothBrush.kt new file mode 100644 index 00000000..a43f078b --- /dev/null +++ b/domain/src/main/java/com/example/domain/toothbrush/model/ToothBrush.kt @@ -0,0 +1,10 @@ +package com.example.domain.toothbrush.model + +import android.graphics.Bitmap +import com.example.domain.common.base.BaseModel +import kotlinx.parcelize.Parcelize + +@Parcelize +class ToothBrush( + val image: ByteArray +): BaseModel \ No newline at end of file diff --git a/domain/src/main/java/com/example/domain/toothbrush/usecase/CheckToothBrushStatusUseCase.kt b/domain/src/main/java/com/example/domain/toothbrush/usecase/CheckToothBrushStatusUseCase.kt new file mode 100644 index 00000000..032f5a71 --- /dev/null +++ b/domain/src/main/java/com/example/domain/toothbrush/usecase/CheckToothBrushStatusUseCase.kt @@ -0,0 +1,18 @@ +package com.example.domain.toothbrush.usecase + +import android.graphics.Bitmap +import com.example.domain.common.base.ResponseState +import com.example.domain.toothbrush.ToothBrushRepository +import com.example.domain.toothbrush.model.ToothBrush +import dagger.Reusable +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +@Reusable +class CheckToothBrushStatusUseCase @Inject constructor( + private val toothBrushRepository: ToothBrushRepository +){ + suspend operator fun invoke(userId: String, image: ByteArray): Flow>{ + return toothBrushRepository.checkToothBrushStatus(userId, image) + } +} \ No newline at end of file diff --git a/presentation/build.gradle b/presentation/build.gradle index ef743ee3..1f7da8c5 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -75,4 +75,15 @@ dependencies { /* BlueView */ implementation 'com.github.Dimezis:BlurView:version-2.0.3' + + /* mlkit */ + implementation 'com.google.mlkit:face-detection:16.1.5' + + /* CameraX */ + def camerax_version = "1.3.0-alpha02" + implementation "androidx.camera:camera-core:${camerax_version}" + implementation "androidx.camera:camera-camera2:${camerax_version}" + implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation "androidx.camera:camera-view:${camerax_version}" + implementation "androidx.camera:camera-extensions:${camerax_version}" } \ No newline at end of file diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index c7a72657..876419d3 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -20,6 +20,19 @@ + + + + + + + + + + + ? = null ): RecyclerView.Adapter() { - inner class ViewHolder(val bind: SubMagazineItemBinding): RecyclerView.ViewHolder(bind.root) + inner class ViewHolder(val bind: SubFullSizeMagazineItemBinding): RecyclerView.ViewHolder(bind.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) - .inflate(R.layout.sub_magazine_item, parent, false) + .inflate(R.layout.sub_full_size_magazine_item, parent, false) - return ViewHolder(SubMagazineItemBinding.bind(view)) + return ViewHolder(SubFullSizeMagazineItemBinding.bind(view)) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { diff --git a/presentation/src/main/java/com/example/smiley/main/MainActivity.kt b/presentation/src/main/java/com/example/smiley/main/MainActivity.kt index 1bab9a25..bf1baacf 100644 --- a/presentation/src/main/java/com/example/smiley/main/MainActivity.kt +++ b/presentation/src/main/java/com/example/smiley/main/MainActivity.kt @@ -8,6 +8,7 @@ import com.example.smiley.R import com.example.smiley.common.extension.setCustomColorStatusBarAndNavigationBar import com.example.smiley.common.listener.FragmentVisibilityListener import com.example.smiley.databinding.ActivityMainBinding +import com.example.smiley.hospital.HospitalMapFragment import com.example.smiley.main.home.HomeFragment import com.example.smiley.main.profile.ProfileFragment import com.example.smiley.main.reserv.ReservFragment @@ -24,12 +25,14 @@ class MainActivity : AppCompatActivity() { private var profileFragment: ProfileFragment? = null private var reservFragment: ReservFragment? = null private var statsFragment: StatsFragment? = null + private var hospitalMapFragment: HospitalMapFragment? = null private var fragmentMap: HashMap? = hashMapOf( R.id.menu_home to homeFragment, R.id.menu_reserv to reservFragment, - R.id.menu_profile to profileFragment, + R.id.menu_map to hospitalMapFragment, R.id.menu_stats to statsFragment, + R.id.menu_profile to profileFragment, ) override fun onCreate(savedInstanceState: Bundle?) { @@ -47,7 +50,7 @@ class MainActivity : AppCompatActivity() { when (it.itemId) { R.id.menu_home -> changeFragment(R.id.menu_home) R.id.menu_reserv -> changeFragment(R.id.menu_reserv) - R.id.menu_inspect -> changeFragment(R.id.menu_inspect) + R.id.menu_map -> changeFragment(R.id.menu_map) R.id.menu_stats -> changeFragment(R.id.menu_stats) R.id.menu_profile -> changeFragment(R.id.menu_profile) else -> changeFragment(R.id.menu_home) @@ -78,7 +81,7 @@ class MainActivity : AppCompatActivity() { return when (fragmentId) { R.id.menu_home -> HomeFragment() R.id.menu_reserv -> ReservFragment() - R.id.menu_inspect -> HomeFragment() + R.id.menu_map -> HospitalMapFragment.newInstance() R.id.menu_stats -> StatsFragment() R.id.menu_profile -> ProfileFragment() else -> HomeFragment() diff --git a/presentation/src/main/java/com/example/smiley/main/banner/adapter/AutoScrollBannerAdapter.kt b/presentation/src/main/java/com/example/smiley/main/banner/adapter/AutoScrollBannerAdapter.kt new file mode 100644 index 00000000..a8a81766 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/main/banner/adapter/AutoScrollBannerAdapter.kt @@ -0,0 +1,73 @@ +package com.example.smiley.main.banner.adapter + +import android.annotation.SuppressLint +import android.content.Context +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.example.domain.banner.Banner +import com.example.domain.banner.BannerType +import com.example.smiley.R +import com.example.smiley.common.extension.getViewDataBinding +import com.example.smiley.common.listener.TransparentTouchListener +import com.example.smiley.databinding.SubBannerItemBinding + +class AutoScrollBannerAdapter( + private val context: Context, + private var items: ArrayList = arrayListOf( + Banner( + id = 1, + imageRes = R.drawable.img_banner_medicine, + type = BannerType.BANNER_MEDICINE + ), + Banner( + id = 2, + imageRes = R.drawable.img_banner_ai, + type = BannerType.BANNER_AI + ), + Banner( + id = 3, + imageRes = R.drawable.img_banner_chat_bot, + type = BannerType.BANNER_CHAT_BOT + ), + Banner( + id = 4, + imageRes = R.drawable.img_banner_find_dental, + type = BannerType.BANNER_NEAR_BY_HOSPITAL + ), + ) +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + getViewDataBinding(parent, R.layout.sub_banner_item) + ) + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + @SuppressLint("NotifyDataSetChanged") + fun updateDataSet(items: ArrayList) { + this.items = items + notifyDataSetChanged() + } + + inner class ViewHolder( + private val bind: SubBannerItemBinding + ) : RecyclerView.ViewHolder(bind.root) { + @SuppressLint("ClickableViewAccessibility") + fun bind(item: Banner) { + with(bind) { + Glide.with(context) + .load(item.imageRes) + .into(ivBanner) + + ivBanner.setOnTouchListener(TransparentTouchListener()) + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/main/home/HomeFragment.kt b/presentation/src/main/java/com/example/smiley/main/home/HomeFragment.kt index 277af5ee..8fa4dfe0 100644 --- a/presentation/src/main/java/com/example/smiley/main/home/HomeFragment.kt +++ b/presentation/src/main/java/com/example/smiley/main/home/HomeFragment.kt @@ -1,15 +1,25 @@ package com.example.smiley.main.home +import android.annotation.SuppressLint +import android.graphics.Color +import android.graphics.Typeface import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.children import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.example.domain.hospital.model.SimpleHospital import com.example.domain.magazine.model.Magazine import com.example.domain.youtube.model.YoutubeVideo @@ -18,26 +28,51 @@ import com.example.smiley.R import com.example.smiley.bluetooth.viewmodel.BluetoothDataState import com.example.smiley.bluetooth.viewmodel.BluetoothViewModel import com.example.smiley.common.extension.addFragmentToFullScreen +import com.example.smiley.common.extension.invisible import com.example.smiley.common.extension.repeatOnStarted -import com.example.smiley.common.extension.setBasicMode +import com.example.smiley.common.extension.setWearTimeMode import com.example.smiley.common.extension.showToast import com.example.smiley.common.extension.stop +import com.example.smiley.common.extension.visible import com.example.smiley.common.listener.FragmentVisibilityListener import com.example.smiley.common.listener.OnItemClickListener import com.example.smiley.common.utils.NotifyManager +import com.example.smiley.common.utils.decorutils.SideSpaceDecoration import com.example.smiley.common.view.BaseFragment import com.example.smiley.databinding.FragmentHomeBinding import com.example.smiley.databinding.LayoutCommonAppBarBinding import com.example.smiley.hospital.HospitalMapFragment import com.example.smiley.magazine.MagazineDetailFragment import com.example.smiley.magazine.MagazineListFragment +import com.example.smiley.main.banner.adapter.AutoScrollBannerAdapter +import com.example.smiley.main.home.adapter.magazine.RecentMagazineAdapter import com.example.smiley.main.home.adapter.partner.PartnerListAdapter -import com.example.smiley.main.home.adapter.timeline.TimeLineAdapter -import com.example.smiley.main.home.adapter.timeline.TimeLineItem import com.example.smiley.main.home.adapter.youtube.YoutubeListAdapter import com.example.smiley.main.home.viewmodel.HomeFragmentState import com.example.smiley.main.home.viewmodel.HomeViewModel +import com.github.mikephil.charting.components.AxisBase +import com.github.mikephil.charting.components.Description +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.data.LineData +import com.github.mikephil.charting.data.LineDataSet +import com.github.mikephil.charting.formatter.ValueFormatter +import com.kizitonwose.calendar.core.WeekDay +import com.kizitonwose.calendar.core.WeekDayPosition +import com.kizitonwose.calendar.core.atStartOfMonth +import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.view.ViewContainer +import com.kizitonwose.calendar.view.WeekDayBinder import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.jetbrains.annotations.TestOnly +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.YearMonth +import java.time.format.TextStyle +import java.util.Locale /** * A simple [Fragment] subclass. @@ -46,28 +81,41 @@ import dagger.hilt.android.AndroidEntryPoint */ @AndroidEntryPoint class HomeFragment : BaseFragment(), FragmentVisibilityListener { + private var _bind: FragmentHomeBinding?=null private val bind: FragmentHomeBinding get() = _bind!! - private val appBarBinding: LayoutCommonAppBarBinding by lazy { LayoutCommonAppBarBinding.bind(bind.clContainer) } + private val appBarBinding: LayoutCommonAppBarBinding by lazy { + LayoutCommonAppBarBinding.bind(bind.clParent) + } private val bluetoothVm: BluetoothViewModel by viewModels({requireActivity()}) private val homeVm: HomeViewModel by viewModels() + private val bannerAdapter: AutoScrollBannerAdapter by lazy { + context?.let { + AutoScrollBannerAdapter(it) + }!! + } private var notifyFlag:Boolean = false override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { // Inflate the layout for this fragment _bind = DataBindingUtil.inflate(inflater, R.layout.fragment_home, container, false) observe() initView() + initCalendarView() + initStatusChart() addClickEvent() - initTimeLineView() + + initBannerView() + initMagazineListView() initPartnerListView() initYoutubeListView() + requestData() return bind.root @@ -79,16 +127,189 @@ class HomeFragment : BaseFragment(), FragmentVisibilityListener { } private fun initView(){ - bind.llMagazineDetailBtn.setOnClickListener { + bind.tvTodayMagazine.setOnClickListener { (requireActivity() as AppCompatActivity) .addFragmentToFullScreen(MagazineListFragment.newInstance()) } App.user?.let { - bind.tvUserName.text = "${it.name}님" + bind.tvNickname.text = "${it.name}님" } } + /**--------------------------------------------------------------------------------------------*/ + private val today = LocalDate.now() + private fun initCalendarView(){ + val currentMonth = YearMonth.now() + val endMonth = currentMonth.plusMonths(1) + val firstDayOfWeek = firstDayOfWeekFromLocale() + + initWeekCalendar(currentMonth, endMonth, daysOfWeek(firstDayOfWeek)) + } + + @SuppressLint("ClickableViewAccessibility") + private fun initWeekCalendar(startMonth: YearMonth, endMonth: YearMonth, daysOfWeek: List){ + with(bind.wcvWeekWearStatus){ + dayBinder = object : WeekDayBinder { + override fun create(view: View): WeekDayViewContainer = WeekDayViewContainer(view) + override fun bind(container: WeekDayViewContainer, data: WeekDay) { + container.day = data + bindDate( + date = data.date, + container = container, + isSelectable = data.position == WeekDayPosition.RangeDate + ) + } + } + + setup( + startMonth.atStartOfMonth(), + endMonth.atEndOfMonth(), + daysOfWeek.first() + ) + scrollToWeek(today) + setOnTouchListener { _, _ -> true } + } + } + + /** + * @param cnt Int 날짜 셀의 착용 단계를 표현 (추후 시간 데이터로 바꿔야함) + */ + private fun bindDate(date: LocalDate, container: WeekDayViewContainer, isSelectable: Boolean){ + with(container) { + tvDayOfWeek.text = date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()) + tvDay.text = "${date.dayOfMonth}" + setDot() + } + } + + inner class WeekDayViewContainer(view: View): ViewContainer(view){ + lateinit var day: WeekDay + val tvDayOfWeek: TextView = view.findViewById(R.id.tvDayOfWeek) + val tvDay: TextView = view.findViewById(R.id.tvDay) + val dotLayout: LinearLayout = view.findViewById(R.id.ll_dot_layout) + + fun setEnabled(isEnabled: Boolean){ + if(isEnabled){ + this.tvDay.setTextColor(ContextCompat.getColor(requireActivity(), R.color.black1_20)) + } else { + this.tvDay.setTextColor(ContextCompat.getColor(requireActivity(), R.color.gray5_CB)) + } + } + + @TestOnly + fun setDot(){ + val dotCnt = 1..4 + val dots = dotLayout.children.toList() + val cnt = dotCnt.random() + + for (i in dots.indices){ + if(i < cnt){ + dots[i].visible() + } else { + dots[i].invisible() + } + } + } + } + /**--------------------------------------------------------------------------------------------*/ + + private fun initStatusChart(labelList: ArrayList = arrayListOf( + "14일", "15일", "16일", "17일", "18일", "19일", "오늘" + )){ + context ?: return + + with(bind){ + val xAxis = lcWeekStatusChart.xAxis.apply { + position = XAxis.XAxisPosition.BOTTOM + isGranularityEnabled = true + valueFormatter = object : ValueFormatter() { + override fun getAxisLabel(value: Float, axis: AxisBase?): String { + val index = value.toInt() - 1 + return if(index >= 0 && index < labelList.size){ + labelList[index] + } else { + "" + } + } + } + + // 하단 라벨 텍스트 설정 + typeface = Typeface.create(ResourcesCompat.getFont(requireContext(), R.font.pretendard_semibold), Typeface.NORMAL) + textSize = 12f + textColor = ContextCompat.getColor(requireContext(), R.color.gray3_82) + + // 세로 점선 설정 (그리드 라인) + setDrawGridLines(true) + enableGridDashedLine(15f, 10f, 0f) + gridLineWidth = 1.5f + gridColor = ContextCompat.getColor(requireContext(), R.color.gray6_F5) + + // 기타 + setDrawAxisLine(false) + yOffset = 15f + } + + val yLeftAxis = lcWeekStatusChart.axisLeft.apply { + xOffset = 15f + axisMaximum = 1440f + axisMinimum = -200f // 최솟값이 0일때 Min을 0으로 설정하면 그래프가 얇아지는 현상 발생 (최소값에 여유를 두어 해결) + + setDrawLabels(false) + setDrawAxisLine(false) + setDrawGridLines(false) + } + + lcWeekStatusChart.axisRight.isEnabled = false + + // 차트 범례 + val legend = lcWeekStatusChart.legend + legend.isEnabled = false + } + + initChartData() + } + + private fun initChartData( + entries: ArrayList = arrayListOf( + Entry(1f, 705f), + Entry(2f, 1000f), + Entry(3f, 0f), + Entry(4f, 1201f), + Entry(5f, 150f), + Entry(6f, 0f), + Entry(7f, 3f), + ) + ) { + context ?: return + + val lineDataSet = LineDataSet(entries, null).apply { + mode = LineDataSet.Mode.CUBIC_BEZIER // 곡선으로 설정 + color = ContextCompat.getColor(requireContext(), R.color.primary_normal_light) + lineWidth = 2f + + setDrawValues(false) // 점마다 값 표시를 제거 + setDrawHighlightIndicators(false) // 눌렀을 때 하이라이트 제거 + + /* 인디케이터 세팅 */ + setCircleColor(ContextCompat.getColor(requireContext(), R.color.primary_normal_light)) + circleHoleColor = Color.WHITE + circleRadius = 5.5f + circleHoleRadius = 4f + } + + val lineData = LineData(lineDataSet) + + with(bind.lcWeekStatusChart){ + data = lineData + isDoubleTapToZoomEnabled = false + description = Description().apply { text = "" } + extraBottomOffset = 20f // 하단 x축 라벨 잘림 방지 여백 + resetViewPortOffsets() + } + } + + /**--------------------------------------------------------------------------------------------*/ private fun addClickEvent(){ bind.tvFamousDentalHospital.setOnClickListener { this.addFragmentToFullScreen(HospitalMapFragment.newInstance()) @@ -104,7 +325,7 @@ class HomeFragment : BaseFragment(), FragmentVisibilityListener { } private fun requestData(){ - homeVm.getTimeLineData() + homeVm.getMagazineList(5) homeVm.getNearByPartnerList(3) homeVm.getRecommendVideoList() } @@ -135,6 +356,7 @@ class HomeFragment : BaseFragment(), FragmentVisibilityListener { } notifyFlag = state.wearFlag + appBarBinding.setWearTimeMode(state.wearFlag) } } } @@ -144,15 +366,18 @@ class HomeFragment : BaseFragment(), FragmentVisibilityListener { homeVm.state.collect { state -> when(state){ is HomeFragmentState.Init -> Unit - is HomeFragmentState.TimeLine -> { - handleTimeLine(state.timeLine) - } + is HomeFragmentState.PartnerHospital -> { handlePartnerHospital(state.hospitals) } is HomeFragmentState.RecommendVideo -> { handleRecommendVideo(state.youtubeList) } + + is HomeFragmentState.RecentMagazine -> { + handleMagazine(state.magazine) + } + is HomeFragmentState.Error -> { Log.e("HomeFragment", state.message) } @@ -163,11 +388,11 @@ class HomeFragment : BaseFragment(), FragmentVisibilityListener { } } - private fun handleTimeLine(timelineItem: List){ - bind.sflShimmerLayout.stop() + private fun handleMagazine(magazines: ArrayList){ + bind.sflRecentMagazine.stop() - val adapter = bind.rvTimelineView.adapter as TimeLineAdapter - adapter.changeDataSet(timelineItem as ArrayList) + val adapter = bind.rvRecentMagazine.adapter as RecentMagazineAdapter + adapter.updateDataSet(magazines) } private fun handlePartnerHospital(hospitals: List){ @@ -184,18 +409,36 @@ class HomeFragment : BaseFragment(), FragmentVisibilityListener { adapter.updateDataSet(youtubeList = youtubeList) } - /** - * 타임라인 초기화 - */ - private fun initTimeLineView(){ - bind.rvTimelineView.apply { - adapter = TimeLineAdapter( + private fun initBannerView(){ + context ?: return + + bind.vpBanner.apply { + adapter = bannerAdapter + // 코루틴으로 자동 스크롤 처리 + lifecycleScope.launch { + var current = 0 + while (true) { + delay(3000) + + this@apply.setCurrentItem((current++ % bannerAdapter.itemCount), true) + } + } + } + } + + private fun initMagazineListView(){ + context ?: return + + bind.rvRecentMagazine.apply { + adapter = RecentMagazineAdapter( + context, arrayListOf(), magazineClickListener ) setHasFixedSize(true) - layoutManager = LinearLayoutManager(requireActivity()) + addItemDecoration(SideSpaceDecoration(context, 10)) + layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) } } @@ -228,26 +471,17 @@ class HomeFragment : BaseFragment(), FragmentVisibilityListener { override fun onShowFragment() { super.onShowFragment() - appBarBinding.setBasicMode() + appBarBinding.setWearTimeMode() } private val magazineClickListener = object : OnItemClickListener { override fun onItemClicked(view: View, data: Magazine) { val magazineFragment = MagazineDetailFragment.newInstance(data.contentUrl) - (requireActivity() as AppCompatActivity) - .addFragmentToFullScreen(magazineFragment) + this@HomeFragment.addFragmentToFullScreen(magazineFragment) } } companion object { - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment HomeFragment. - */ @JvmStatic fun newInstance() = HomeFragment() } diff --git a/presentation/src/main/java/com/example/smiley/main/home/adapter/magazine/RecentMagazineAdapter.kt b/presentation/src/main/java/com/example/smiley/main/home/adapter/magazine/RecentMagazineAdapter.kt new file mode 100644 index 00000000..83cffc78 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/main/home/adapter/magazine/RecentMagazineAdapter.kt @@ -0,0 +1,70 @@ +package com.example.smiley.main.home.adapter.magazine + +import android.annotation.SuppressLint +import android.content.Context +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.example.domain.magazine.model.Magazine +import com.example.smiley.R +import com.example.smiley.common.extension.getViewDataBinding +import com.example.smiley.common.listener.OnItemClickListener +import com.example.smiley.common.listener.TransparentTouchListener +import com.example.smiley.databinding.SubCommonTileItemBinding +import eightbitlab.com.blurview.RenderScriptBlur + +class RecentMagazineAdapter( + private val context:Context, + private var items: ArrayList, + private val magazineClickListener: OnItemClickListener? +): RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + getViewDataBinding(parent, R.layout.sub_common_tile_item) + ) + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + @SuppressLint("NotifyDataSetChanged") + fun updateDataSet(items: ArrayList){ + this.items = items + notifyDataSetChanged() + } + + inner class ViewHolder( + private val bind: SubCommonTileItemBinding + ) : RecyclerView.ViewHolder(bind.root) { + @SuppressLint("ClickableViewAccessibility") + fun bind(item: Magazine){ + with(bind){ + tvType.text = "덴탈 매거진" + tvTitle.text = item.title + + Glide.with(context) + .load(item.thumbnail) + .into(ivThumbnail) + + val decorView = bind.root + val rootView = decorView.findViewById(R.id.cvParent) + val windowBackground = decorView.background + + with(bind.blurViewDimd) { + setupWith(rootView, RenderScriptBlur(context)) + .setFrameClearDrawable(windowBackground) + .setBlurRadius(5f) + } + } + bind.cvParent.setOnClickListener { + magazineClickListener?.onItemClicked(it, item) + } + + bind.cvParent.setOnTouchListener(TransparentTouchListener()) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/main/home/adapter/timeline/TimeLineAdapter.kt b/presentation/src/main/java/com/example/smiley/main/home/adapter/timeline/TimeLineAdapter.kt index 662ede79..3016bf0b 100644 --- a/presentation/src/main/java/com/example/smiley/main/home/adapter/timeline/TimeLineAdapter.kt +++ b/presentation/src/main/java/com/example/smiley/main/home/adapter/timeline/TimeLineAdapter.kt @@ -62,7 +62,7 @@ class TimeLineAdapter( } @SuppressLint("NotifyDataSetChanged") - fun changeDataSet(items: ArrayList){ + fun updateDataSet(items: ArrayList){ this.items = items notifyDataSetChanged() } diff --git a/presentation/src/main/java/com/example/smiley/main/home/adapter/youtube/YoutubeListAdapter.kt b/presentation/src/main/java/com/example/smiley/main/home/adapter/youtube/YoutubeListAdapter.kt index 8e6dd18a..951cdb53 100644 --- a/presentation/src/main/java/com/example/smiley/main/home/adapter/youtube/YoutubeListAdapter.kt +++ b/presentation/src/main/java/com/example/smiley/main/home/adapter/youtube/YoutubeListAdapter.kt @@ -2,6 +2,8 @@ package com.example.smiley.main.home.adapter.youtube import android.annotation.SuppressLint import android.content.Context +import android.content.Intent +import android.net.Uri import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil @@ -47,7 +49,10 @@ class YoutubeListAdapter( .load(item.thumbnail) .into(ivThumbnail) } - + bind.clContainer.setOnClickListener { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(item.youtubeUrl)) + context.startActivity(intent) + } bind.clContainer.setOnTouchListener(TransparentTouchListener()) } } diff --git a/presentation/src/main/java/com/example/smiley/main/home/viewmodel/HomeViewModel.kt b/presentation/src/main/java/com/example/smiley/main/home/viewmodel/HomeViewModel.kt index 529b670f..049f42a3 100644 --- a/presentation/src/main/java/com/example/smiley/main/home/viewmodel/HomeViewModel.kt +++ b/presentation/src/main/java/com/example/smiley/main/home/viewmodel/HomeViewModel.kt @@ -5,13 +5,11 @@ import androidx.lifecycle.viewModelScope import com.example.domain.common.base.ResponseState import com.example.domain.hospital.model.SimpleHospital import com.example.domain.hospital.usecase.GetNearByPartnerHospitalUseCase +import com.example.domain.magazine.model.Magazine import com.example.domain.magazine.usecase.GetRecentMagazineUseCase import com.example.domain.youtube.model.YoutubeVideo import com.example.domain.youtube.usecase.GetRecommendVideoUseCase import com.example.smiley.common.base.BaseViewModel -import com.example.smiley.main.home.adapter.timeline.TimeLineItem -import com.example.smiley.main.home.adapter.timeline.TimeLineObject -import com.example.smiley.main.home.adapter.timeline.ViewType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.catch @@ -26,35 +24,15 @@ class HomeViewModel @Inject constructor( private val getRecommendVideoUseCase: GetRecommendVideoUseCase ): BaseViewModel() { - fun getTimeLineData(){ - viewModelScope.launch(Dispatchers.IO){ - getRecentMagazineUseCase(2) - .onStart { - // Skeleton - Log.d("매거진 조회 요청", "요청 보냄") - } - .catch { - setState(HomeFragmentState.ShowToast(message = it.message.toString())) - Log.e("타임라인 조회 에러", it.message.toString()) - } + fun getMagazineList(cnt: Int){ + viewModelScope.launch { + getRecentMagazineUseCase(cnt) + .onStart { } + .catch { setState(HomeFragmentState.ShowToast(message = it.message.toString())) } .collect { state -> when(state){ is ResponseState.Success -> { - val timeLineItems = arrayListOf().apply { - state.data.magazines.forEach { - add( - TimeLineItem( - viewType = ViewType.MAGAZINE_OBJECT.name, - viewObject = TimeLineObject.MagazineObject( - notice = it.title.replace("\n", " "), - magazine = it - ) - ) - ) - } - } - - setState(HomeFragmentState.TimeLine(timeLineItems)) + setState(HomeFragmentState.RecentMagazine(state.data.magazines)) } is ResponseState.Error -> { setState(HomeFragmentState.Error(state.error.message)) @@ -108,7 +86,7 @@ class HomeViewModel @Inject constructor( sealed class HomeFragmentState { object Init: HomeFragmentState() - data class TimeLine(val timeLine: List): HomeFragmentState() + data class RecentMagazine(val magazine: ArrayList): HomeFragmentState() data class PartnerHospital(val hospitals: List): HomeFragmentState() data class RecommendVideo(val youtubeList: ArrayList): HomeFragmentState() data class Error(val message: String): HomeFragmentState() diff --git a/presentation/src/main/java/com/example/smiley/main/profile/ProfileFragment.kt b/presentation/src/main/java/com/example/smiley/main/profile/ProfileFragment.kt index 1bf50226..11bae9c0 100644 --- a/presentation/src/main/java/com/example/smiley/main/profile/ProfileFragment.kt +++ b/presentation/src/main/java/com/example/smiley/main/profile/ProfileFragment.kt @@ -9,7 +9,7 @@ import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import com.example.smiley.App import com.example.smiley.R -import com.example.smiley.selfassessment.SelfAssessmentFragment +import com.example.smiley.selfassessment.fragment.SelfAssessmentFragment import com.example.smiley.bluetooth.fragment.BluetoothSearchFragment import com.example.smiley.common.extension.addFragmentToFullScreen import com.example.smiley.common.extension.setBasicMode @@ -21,7 +21,6 @@ import com.example.smiley.hospital.HospitalMapFragment import com.example.smiley.hospital.HospitalSearchFragment import com.example.smiley.magazine.MagazineListFragment import com.example.smiley.medicine.MedicineCheckFragment -import com.example.smiley.medicine.MedicineSearchFragment // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER diff --git a/presentation/src/main/java/com/example/smiley/main/stats/StatsFragment.kt b/presentation/src/main/java/com/example/smiley/main/stats/StatsFragment.kt index f61b2b79..e41f97ce 100644 --- a/presentation/src/main/java/com/example/smiley/main/stats/StatsFragment.kt +++ b/presentation/src/main/java/com/example/smiley/main/stats/StatsFragment.kt @@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat import androidx.core.view.children import androidx.core.view.updateLayoutParams import androidx.databinding.DataBindingUtil -import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager import com.example.domain.common.base.NetworkError @@ -24,7 +23,6 @@ import com.example.smiley.common.extension.getNumberOfWeeks import com.example.smiley.common.extension.repeatOnStarted import com.example.smiley.common.extension.setCalendarMode import com.example.smiley.common.extension.setDateText -import com.example.smiley.common.extension.setTitle import com.example.smiley.common.extension.showConfirmDialog import com.example.smiley.common.listener.FragmentVisibilityListener import com.example.smiley.common.view.BaseFragment @@ -40,6 +38,7 @@ import com.github.mikephil.charting.data.PieData import com.github.mikephil.charting.data.PieDataSet import com.github.mikephil.charting.data.PieEntry import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDay @@ -48,6 +47,7 @@ import com.kizitonwose.calendar.core.atStartOfMonth import com.kizitonwose.calendar.core.daysOfWeek import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale import com.kizitonwose.calendar.view.MonthDayBinder +import com.kizitonwose.calendar.view.MonthScrollListener import com.kizitonwose.calendar.view.ViewContainer import com.kizitonwose.calendar.view.WeekDayBinder import com.kizitonwose.calendar.view.WeekScrollListener @@ -59,16 +59,6 @@ import java.time.YearMonth import java.time.format.TextStyle import java.util.Locale - -// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -private const val ARG_PARAM1 = "param1" -private const val ARG_PARAM2 = "param2" - -/** - * A simple [Fragment] subclass. - * Use the [StatsFragment.newInstance] factory method to - * create an instance of this fragment. - */ @AndroidEntryPoint class StatsFragment : BaseFragment(), FragmentVisibilityListener { @@ -93,17 +83,6 @@ class StatsFragment : BaseFragment(), FragmentVisibilityListener { private var prevContainer: WeekDayViewContainer? = null private val today = LocalDate.now() - private var param1: String? = null - private var param2: String? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - arguments?.let { - param1 = it.getString(ARG_PARAM1) - param2 = it.getString(ARG_PARAM2) - } - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -237,8 +216,8 @@ class StatsFragment : BaseFragment(), FragmentVisibilityListener { private fun initCalendarView(){ val currentMonth = YearMonth.now() - val startMonth = currentMonth.minusMonths(240) - val endMonth = currentMonth.plusMonths(240) + val startMonth = currentMonth.minusMonths(3) + val endMonth = currentMonth.plusMonths(3) val firstDayOfWeek = firstDayOfWeekFromLocale() initCalendarTitle() @@ -391,29 +370,15 @@ class StatsFragment : BaseFragment(), FragmentVisibilityListener { } } - companion object { - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment StatsFragment. - */ - @JvmStatic - fun newInstance(param1: String, param2: String) = - StatsFragment().apply { - arguments = Bundle().apply { - putString(ARG_PARAM1, param1) - putString(ARG_PARAM2, param2) - } - } - } - override fun onShowFragment() { appBarBinding.setCalendarMode() appBarBinding.setDateText( String.format(resources.getString(R.string.date_year_month_kor),today.year, today.monthValue) ) } + + companion object { + @JvmStatic + fun newInstance() = StatsFragment() + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/selfassessment/adapter/AssessmentAdapter.kt b/presentation/src/main/java/com/example/smiley/selfassessment/adapter/assessment/AssessmentAdapter.kt similarity index 66% rename from presentation/src/main/java/com/example/smiley/selfassessment/adapter/AssessmentAdapter.kt rename to presentation/src/main/java/com/example/smiley/selfassessment/adapter/assessment/AssessmentAdapter.kt index fbf858e5..a87444d0 100644 --- a/presentation/src/main/java/com/example/smiley/selfassessment/adapter/AssessmentAdapter.kt +++ b/presentation/src/main/java/com/example/smiley/selfassessment/adapter/assessment/AssessmentAdapter.kt @@ -1,31 +1,33 @@ -package com.example.smiley.selfassessment.adapter +package com.example.smiley.selfassessment.adapter.assessment +import android.annotation.SuppressLint import android.content.Context import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy import com.example.smiley.R import com.example.smiley.common.extension.getViewDataBinding +import com.example.smiley.common.listener.OnItemClickListener import com.example.smiley.common.listener.TransparentTouchListener import com.example.smiley.databinding.SubCommonTileItemBinding +import com.example.smiley.selfassessment.item.AssessmentItem import eightbitlab.com.blurview.RenderScriptBlur class AssessmentAdapter( private val context: Context, - private val itemList: ArrayList + private val itemList: ArrayList, + private val onItemClickListener: OnItemClickListener ): RecyclerView.Adapter() { - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): ViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( getViewDataBinding(parent, R.layout.sub_common_tile_item) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(itemList[position]) + holder.bind(itemList[position], position) } override fun getItemCount() = itemList.size @@ -33,13 +35,15 @@ class AssessmentAdapter( inner class ViewHolder( private val bind: SubCommonTileItemBinding ) : RecyclerView.ViewHolder(bind.root) { - fun bind(item: AssessmentItem){ + @SuppressLint("ClickableViewAccessibility") + fun bind(item: AssessmentItem, position: Int){ with(bind){ tvType.text = item.type tvTitle.text = item.title Glide.with(context) .load(item.imageRes) + .diskCacheStrategy(DiskCacheStrategy.NONE) .into(ivThumbnail) val decorView = bind.root @@ -52,6 +56,10 @@ class AssessmentAdapter( .setBlurRadius(5f) } + cvParent.setOnClickListener { + onItemClickListener.onItemClicked(position, item) + } + cvParent.setOnTouchListener(TransparentTouchListener()) } } diff --git a/presentation/src/main/java/com/example/smiley/selfassessment/adapter/assessment/AssessmentResultAdapter.kt b/presentation/src/main/java/com/example/smiley/selfassessment/adapter/assessment/AssessmentResultAdapter.kt new file mode 100644 index 00000000..79eb9b8a --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/selfassessment/adapter/assessment/AssessmentResultAdapter.kt @@ -0,0 +1,70 @@ +package com.example.smiley.selfassessment.adapter.assessment + +import android.content.Context +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.example.domain.assessment.model.Assessment +import com.example.smiley.R +import com.example.smiley.common.extension.getViewDataBinding +import com.example.smiley.selfassessment.viewholder.AssessmentResultViewHolder + +class AssessmentResultAdapter( + private val context: Context, + private val itemList: ArrayList +) : RecyclerView.Adapter() { + + enum class HolderType { + HEADER, + STATS, + FAQ, + RECOMMEND_VIDEO + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AssessmentResultViewHolder { + return when(viewType){ + HolderType.HEADER.ordinal -> { + AssessmentResultViewHolder.Header( + getViewDataBinding(parent, R.layout.layout_assessment_header) + ) + } + HolderType.STATS.ordinal -> { + AssessmentResultViewHolder.Stats( + getViewDataBinding(parent, R.layout.layout_assessment_stats) + ) + } + HolderType.FAQ.ordinal -> { + AssessmentResultViewHolder.Faq( + getViewDataBinding(parent, R.layout.layout_assessment_faq) + ) + } + else -> { + AssessmentResultViewHolder.RecommendVideo( + getViewDataBinding(parent, R.layout.layout_assessment_recommend_video) + ) + } + } + } + + override fun onBindViewHolder(holder: AssessmentResultViewHolder, position: Int) { + holder.bind(itemList[position]) + } + + override fun getItemCount() = itemList.size + + override fun getItemViewType(position: Int): Int { + return when (itemList[position]) { + is Assessment.Header -> { + HolderType.HEADER + } + is Assessment.Stats -> { + HolderType.STATS + } + is Assessment.FaqList -> { + HolderType.FAQ + } + is Assessment.RecommendVideoList -> { + HolderType.RECOMMEND_VIDEO + } + }.ordinal + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/selfassessment/adapter/faq/ExpandFaqAdater.kt b/presentation/src/main/java/com/example/smiley/selfassessment/adapter/faq/ExpandFaqAdater.kt new file mode 100644 index 00000000..5cc7e966 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/selfassessment/adapter/faq/ExpandFaqAdater.kt @@ -0,0 +1,59 @@ +package com.example.smiley.selfassessment.adapter.faq + +import android.annotation.SuppressLint +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.RecyclerView +import com.example.domain.assessment.model.Assessment +import com.example.smiley.R +import com.example.smiley.common.customview.ToggleAnimation +import com.example.smiley.common.extension.getViewDataBinding +import com.example.smiley.databinding.SubFaqItemBinding + +class ExpandFaqAdater( + private val faqList: ArrayList +): RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + getViewDataBinding(parent, R.layout.sub_faq_item) + ) + } + + override fun getItemCount(): Int = faqList.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(faqList[position], position) + } + + /** + * ExpandLayout 토글 메소드 + */ + private fun toggleLayout(isExpanded: Boolean, view: View, expandLayout: ConstraintLayout): Boolean{ + ToggleAnimation.toggleArrow(view, isExpanded) + + if(isExpanded) ToggleAnimation.expand(expandLayout) + else ToggleAnimation.collapse(expandLayout) + + return isExpanded + } + + inner class ViewHolder( + private val bind: SubFaqItemBinding + ): RecyclerView.ViewHolder(bind.root){ + @SuppressLint("SetTextI18n") + fun bind(item: Assessment.FaqList.Faq, position: Int){ + with(bind){ + tvQuestionIdx.text = "Q${position+1}." + tvQuestion.text = item.question + tvAnswer.text = item.answer + + clQuestionLayout.setOnClickListener { + val show = toggleLayout(!item.isExpanded, ivArrayDown, clExpandLayout) + item.isExpanded = show + } + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/selfassessment/adapter/recommendvideo/RecommendVideoAdater.kt b/presentation/src/main/java/com/example/smiley/selfassessment/adapter/recommendvideo/RecommendVideoAdater.kt new file mode 100644 index 00000000..1c8cae03 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/selfassessment/adapter/recommendvideo/RecommendVideoAdater.kt @@ -0,0 +1,58 @@ +package com.example.smiley.selfassessment.adapter.recommendvideo + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.example.domain.assessment.model.Assessment +import com.example.smiley.R +import com.example.smiley.common.extension.getViewDataBinding +import com.example.smiley.common.listener.TransparentTouchListener +import com.example.smiley.databinding.SubRecommendVideoItemBinding + +class RecommendVideoAdater( + private val context:Context, + private val videoList: ArrayList +): RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + getViewDataBinding(parent, R.layout.sub_recommend_video_item) + ) + } + + override fun getItemCount(): Int = videoList.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(videoList[position], position) + } + + inner class ViewHolder( + private val bind: SubRecommendVideoItemBinding + ): RecyclerView.ViewHolder(bind.root){ + @SuppressLint("SetTextI18n", "ClickableViewAccessibility") + fun bind(item: Assessment.RecommendVideoList.RecommendVideo, position: Int){ + with(bind){ + tvVideoTitle.text = item.title + val resources = item.thumbnail ?: item.thumbnailRes + + Glide.with(context) + .load(resources) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(ivThumbnail) + + // 유튜브앱 랜딩 + clContainer.setOnClickListener { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(item.youtubeUrl)) + context.startActivity(intent) + } + + clContainer.setOnTouchListener(TransparentTouchListener()) + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/selfassessment/fragment/AssessmentResultFragment.kt b/presentation/src/main/java/com/example/smiley/selfassessment/fragment/AssessmentResultFragment.kt new file mode 100644 index 00000000..70005c67 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/selfassessment/fragment/AssessmentResultFragment.kt @@ -0,0 +1,227 @@ +package com.example.smiley.selfassessment.fragment + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Bundle +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide +import com.example.domain.assessment.model.Assessment +import com.example.smiley.App +import com.example.smiley.R +import com.example.smiley.common.base.BaseFragment +import com.example.smiley.common.extension.gone +import com.example.smiley.common.extension.repeatOnStarted +import com.example.smiley.common.extension.showToast +import com.example.smiley.common.extension.visible +import com.example.smiley.common.utils.decorutils.AllSpaceDecoration +import com.example.smiley.common.utils.decorutils.EdgeSpaceDecoration +import com.example.smiley.databinding.FragmentAssessmentResultBinding +import com.example.smiley.selfassessment.adapter.assessment.AssessmentResultAdapter +import com.example.smiley.test_ai_assessment.AssessmentState +import com.example.smiley.test_ai_assessment.FaceDetectViewModel +import dagger.hilt.android.AndroidEntryPoint +import org.jetbrains.annotations.TestOnly +import java.io.ByteArrayOutputStream +import java.io.File + +@AndroidEntryPoint +class AssessmentResultFragment : BaseFragment(R.layout.fragment_assessment_result) { + + companion object { + const val IMAGE_PATH: String = "IMAGE_PATH" + const val IMAGE: String = "IMAGE" + const val ASSESSMENT_TYPE = "ASSESSMENT_TYPE" + + @JvmStatic + fun newInstance(imagePath: String? = null, image: ByteArray? = null, type: AssessmentType) = AssessmentResultFragment().apply { + arguments = Bundle().apply { + putString(IMAGE_PATH, imagePath) + putByteArray(IMAGE, image) + putString(ASSESSMENT_TYPE, type.name) + } + } + } + + enum class AssessmentType { + FACIAL, + SIDE_FACE, + TOOTH_BRUSH + } + + private var imagePath: String? = null + private var image: ByteArray? = null + private var assessmentType: AssessmentType? = null + + private val detectVm: FaceDetectViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + arguments?.let { + imagePath = it.getString(IMAGE_PATH) + image = it.getByteArray(IMAGE) + assessmentType = it.getString(ASSESSMENT_TYPE)?.let { name -> + AssessmentType.valueOf(name) + } + } + } + + override fun initView() { + initObserver() + + initLoadingView() + loadImage() + initResultRecyclerView() + } + + private fun initObserver(){ + repeatOnStarted { + observDetectState() + } + } + + private suspend fun observDetectState(){ + detectVm.state.collect { state -> + when(state){ + is AssessmentState.Init -> Unit + is AssessmentState.Error -> { context?.showToast(state.message) } + is AssessmentState.Loading -> { handleLoading(true) } + is AssessmentState.ShowToast -> { context?.showToast(state.message) } + is AssessmentState.ToothBrushResult -> { + handleLoading(false) + + Glide.with(requireContext()) + .load(state.result.image) + .into(bind.ivResult) + } + } + } + } + + private fun handleLoading(isLoading: Boolean){ + if(isLoading) bind.clLoadingView.visible() + else bind.clLoadingView.gone() + } + + private fun loadImage(){ + context ?: return + + // 안면 인식인 경우 + val resultImage = imagePath?.run { + val file = File(this) + val bExist = file.exists() + if (bExist) { + BitmapFactory.decodeFile(this) + } else { + null + } + } ?: return + + when(assessmentType){ + AssessmentType.FACIAL -> { + Glide.with(requireContext()) + .load(resultImage) + .into(bind.ivResult) + } + AssessmentType.TOOTH_BRUSH ->{ + val userId = App.user?.userId ?: "" + val byteArray = ByteArrayOutputStream().apply { + resultImage.compress(Bitmap.CompressFormat.PNG, 50, this) + }.toByteArray() + + detectVm.detectToothBrush(userId, byteArray) + } + else -> {} + } + } + + private fun initResultRecyclerView(){ + context ?: return + with(bind.rvAssessmentResult){ + adapter = AssessmentResultAdapter(context, getAssessmentData()) + addItemDecoration(AllSpaceDecoration(context, 0, 0, 0, 20)) + addItemDecoration(EdgeSpaceDecoration(context, 100, isLastBottomPadding = true)) + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + } + } + + private fun initLoadingView(){ + when(assessmentType){ + AssessmentType.FACIAL -> { + bind.tvTitle.text = "스마일리가 얼굴을 분석 중이에요.\n잠시만 기다려주세요" + } + AssessmentType.TOOTH_BRUSH -> { + bind.clLoadingView.visible() + bind.tvTitle.text = "스마일리가 칫솔모를 분석 중이에요.\n잠시만 기다려주세요" + } + else -> Unit + } + } + + @TestOnly + private fun getAssessmentData(): ArrayList { + return if(assessmentType == AssessmentType.FACIAL){ + arrayListOf( + Assessment.Header( + resultTag = "주의", + title = "${App.user?.name}님의 검사 결과", + explain = "높진 않지만 비대칭이 보이는 것 같아요. 그러나 비대칭이라고 무조건 안 좋은 것은 아니에요! 약간의 비대칭은 오히려 자연스러운 모습으로 보여질 수도 있답니다. 그래도 걱정 된다면 전문의와의 상담을 추천드려요!" + ), + + Assessment.Stats( + facialAsymmetry = 13, + eyeDegree = 23, + lipDegree = 34 + ), + + Assessment.FaqList( + question = resources.getStringArray(R.array.facial_type1_question), + answer = resources.getStringArray(R.array.facial_type1_answer) + ), + Assessment.RecommendVideoList( + getRecommendVideoList() + ), + ) + } else { + arrayListOf( + Assessment.Header( + resultTag = "교체 필요", + title = "칫솔모 검사 결과", + explain = "칫솔 교체가 필요합니다. 눈으로는 손상이 심하지 않더라도, 망가진 칫솔모는 제 기능을 하지 못하며, 치아 손상까지 초래할 수 있습니다. 교정 환자에게는 더욱 치명적이니 꼭 칫솔을 교체해주세요!" + ), + + Assessment.FaqList( + question = resources.getStringArray(R.array.toothbrush_question), + answer = resources.getStringArray(R.array.toothbrush_answer) + ), + Assessment.RecommendVideoList( + getRecommendVideoList() + ), + ) + } + } + + private fun getRecommendVideoList(): ArrayList { + return arrayListOf( + Assessment.RecommendVideoList.RecommendVideo( + title = "안면비대칭 치료방법은? 한의원이냐 경락이냐 치아교정이냐 모든 논란을 끝내준다!", + youtubeUrl = "https://www.youtube.com/watch?v=lPP2HIDSp1g", + thumbnailRes = R.drawable.img_facial_youtube_thumbnail_1 + ), + + Assessment.RecommendVideoList.RecommendVideo( + title = "안면비대칭 유형 구분법 / 안면비대칭 교정운동 / 안면비대칭 자가교정 [교정의 신, 리샘TV]", + youtubeUrl = "https://www.youtube.com/watch?v=HooEhzG0t1U", + thumbnailRes = R.drawable.img_facial_youtube_thumbnail_2 + ), + + Assessment.RecommendVideoList.RecommendVideo( + title = "내 안면비대칭 타입은...? 안면비대칭 타입3와 실제 교정과정! | 정파카", + youtubeUrl = "https://www.youtube.com/watch?v=FnPF60zjUu0", + thumbnailRes = R.drawable.img_facial_youtube_thumbnail_3 + ) + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/selfassessment/fragment/CameraFragment.kt b/presentation/src/main/java/com/example/smiley/selfassessment/fragment/CameraFragment.kt new file mode 100644 index 00000000..012dbd9a --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/selfassessment/fragment/CameraFragment.kt @@ -0,0 +1,215 @@ +package com.example.smiley.selfassessment.fragment + + +import android.Manifest +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.media.Image +import android.os.Build +import android.os.Bundle +import android.view.OrientationEventListener +import android.widget.Toast +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageProxy +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.example.smiley.R +import com.example.smiley.common.base.BaseFragment +import com.example.smiley.common.extension.addFragmentToFullScreen +import com.example.smiley.databinding.FragmentCameraBinding +import com.example.smiley.selfassessment.fragment.AssessmentResultFragment.Companion.ASSESSMENT_TYPE +import com.example.smiley.test_ai_assessment.camera.CameraManager +import com.example.smiley.test_ai_assessment.mlkit.FaceContourDetectionProcessor +import com.example.smiley.test_ai_assessment.util.BitmapSavedCallback +import com.example.smiley.test_ai_assessment.util.drawBitmapOnCanvas +import com.example.smiley.test_ai_assessment.util.imageToBitmap +import com.example.smiley.test_ai_assessment.util.rotateFlipImage +import com.example.smiley.test_ai_assessment.util.saveToGallery +import com.example.smiley.test_ai_assessment.util.scaleImage +import com.google.mlkit.vision.face.Face +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class CameraFragment : BaseFragment(R.layout.fragment_camera) { + + companion object { + private const val REQUEST_CODE_PERMISSIONS = 10 + + @JvmStatic + fun newInstance(type: AssessmentResultFragment.AssessmentType) = CameraFragment().apply { + arguments = Bundle().apply { + putString(ASSESSMENT_TYPE, type.name) + } + } + } + + private var assessmentType: AssessmentResultFragment.AssessmentType? = null + private lateinit var cameraManager: CameraManager + + private val REQUIRED_CAMERA_PERMISSIONS: Array = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // TIRAMISU == Android 13 + arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_MEDIA_IMAGES, + ) + } else { + arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + assessmentType = it.getString(ASSESSMENT_TYPE)?.let { name -> + AssessmentResultFragment.AssessmentType.valueOf(name) + } + } + } + + override fun onPause() { + super.onPause() + + cameraManager.stopCamera() + } + + override fun onResume() { + super.onResume() + + checkPermission() + } + + override fun initView() { + createCameraManager() + + // checkPermission() + addBtnClickEvent() + } + + private fun checkPermission(){ + if (allPermissionsGranted()) { + cameraManager.startCamera() + } else { + ActivityCompat.requestPermissions( + requireActivity(), + REQUIRED_CAMERA_PERMISSIONS, + REQUEST_CODE_PERMISSIONS + ) + } + } + + // 퍼미션 얻는 메소드 + private fun allPermissionsGranted() = REQUIRED_CAMERA_PERMISSIONS.all { + ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED + } + + // 카메라 매니저 생성 메소드 + private fun createCameraManager() { + cameraManager = CameraManager( + context = requireContext(), + lifecycleOwner = this, + finderView = bind.previewViewFinder, + graphicOverlay = bind.graphicOverlayFinder, + analyzer = FaceContourDetectionProcessor( + bind.graphicOverlayFinder, + faceDetectResultCallback + ) + ) + } + + private fun addBtnClickEvent() { + // 카메라 전환 버튼 이벤트 + bind.cameraChangeBtn.setOnClickListener{ + cameraManager.changeCameraSelector() + } + + // 촬영 버튼 클릭 이벤트 + bind.ibCameraBtn.setOnClickListener { + takePicture() + } + } + + /** + * 사진 촬영 메소드 + */ + private fun takePicture() { + Toast.makeText(requireContext(), "take a picture!", Toast.LENGTH_SHORT).show() + + setOrientationEvent() + with(cameraManager){ + imageCapture.takePicture( + cameraExecutor, onImageCaptureCallback + ) + } + } + + /** + * 디바이스의 방향이 변경될 때 카메라의 회전 각도를 설정하는 메소드 + * 방향에 다라 카메라의 회전 각도를 설정 + */ + private fun setOrientationEvent() { + val orientationEventListener = object : OrientationEventListener(requireContext()) { + override fun onOrientationChanged(orientation: Int) { + val rotation: Float = when (orientation) { + in 45..134 -> 270f + in 135..224 -> 180f + in 225..314 -> 90f + else -> 0f + } + cameraManager.rotation = rotation + } + } + orientationEventListener.enable() + } + + /** + * 이미지를 비트맵으로 갤러리에 저장하는 메소드 + * @param image Image + */ + private fun saveImageToGallery(image: Image) { + val appContext = context ?: return + + image.imageToBitmap() + ?.rotateFlipImage(cameraManager.rotation, cameraManager.isFrontMode()) + ?.scaleImage(bind.previewViewFinder, cameraManager.isHorizontalMode()) + ?.let { bitmap -> + val graphicOverlay = bind.graphicOverlayFinder + val processedBitmap = bind.graphicOverlayFinder.processBitmap + + bitmap.drawBitmapOnCanvas(graphicOverlay, cameraManager.isHorizontalMode()) + processedBitmap.saveToGallery(appContext, onBitmapSavedCallback) + } + } + + // FaceContourDetectionProcessor의 처리 결과를 받아오기 위함 + private val faceDetectResultCallback: (face: Face) -> Unit = { + // TODO 로그 찍어서 Face 값이 여러 개가 오진 않는지 확인 + } + + private val onBitmapSavedCallback = object : BitmapSavedCallback { + override fun onSaved(path: String) { + assessmentType ?: return + + this@CameraFragment.addFragmentToFullScreen( + AssessmentResultFragment.newInstance( + imagePath = path, + type = assessmentType!! + ) + ) + } + } + + private val onImageCaptureCallback = object : ImageCapture.OnImageCapturedCallback() { + @SuppressLint("UnsafeExperimentalUsageError", "RestrictedApi") + @androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class) + override fun onCaptureSuccess(image: ImageProxy) { + image.image?.let { + saveImageToGallery(it) + } + + super.onCaptureSuccess(image) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/selfassessment/SelfAssessmentFragment.kt b/presentation/src/main/java/com/example/smiley/selfassessment/fragment/SelfAssessmentFragment.kt similarity index 81% rename from presentation/src/main/java/com/example/smiley/selfassessment/SelfAssessmentFragment.kt rename to presentation/src/main/java/com/example/smiley/selfassessment/fragment/SelfAssessmentFragment.kt index e47db543..f1a1cbcf 100644 --- a/presentation/src/main/java/com/example/smiley/selfassessment/SelfAssessmentFragment.kt +++ b/presentation/src/main/java/com/example/smiley/selfassessment/fragment/SelfAssessmentFragment.kt @@ -1,4 +1,4 @@ -package com.example.smiley.selfassessment +package com.example.smiley.selfassessment.fragment import android.annotation.SuppressLint import android.text.SpannableStringBuilder @@ -10,16 +10,18 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.PagerSnapHelper import com.example.smiley.R import com.example.smiley.common.base.BaseFragment +import com.example.smiley.common.extension.addFragmentToFullScreen import com.example.smiley.common.extension.gone import com.example.smiley.common.extension.resetStatusBarAndNavigationBar import com.example.smiley.common.extension.setCustomColorStatusBarAndNavigationBar import com.example.smiley.common.extension.visibleWithAnimation -import com.example.smiley.common.utils.decorutils.SpaceItemDecoration +import com.example.smiley.common.listener.OnItemClickListener +import com.example.smiley.common.utils.decorutils.SideSpaceDecoration import com.example.smiley.common.utils.textutils.IndentLeadingMarginSpan import com.example.smiley.databinding.FragmentSelfAssessmentBinding import com.example.smiley.databinding.LayoutSubAppBarBinding -import com.example.smiley.selfassessment.adapter.AssessmentAdapter -import com.example.smiley.selfassessment.adapter.AssessmentItem +import com.example.smiley.selfassessment.adapter.assessment.AssessmentAdapter +import com.example.smiley.selfassessment.item.AssessmentItem import eightbitlab.com.blurview.RenderScriptBlur /** @@ -55,10 +57,10 @@ class SelfAssessmentFragment : BaseFragment(R.lay ), ) - private val assessmentAdapter by lazy { + private val assessmentAdapter: AssessmentAdapter by lazy { context?.let { - AssessmentAdapter(it, assessmentList) - } + AssessmentAdapter(it, assessmentList, onAssessmentItemClickListener) + }!! } override fun initView() { @@ -98,12 +100,12 @@ class SelfAssessmentFragment : BaseFragment(R.lay context ?: return val decorView = activity?.window?.decorView - val rootView = decorView?.findViewById(R.id.clParent)!! + val rootView = decorView?.findViewById(R.id.clBlurBasedLayout)!! val windowBackground = decorView.background with(bind.blurViewBackground){ setOnTouchListener { v, event -> true } - setupWith(rootView, RenderScriptBlur(requireContext())) + setupWith(rootView, RenderScriptBlur(context)) .setFrameClearDrawable(windowBackground) .setBlurRadius(5f) } @@ -139,7 +141,7 @@ class SelfAssessmentFragment : BaseFragment(R.lay with(bind.rvAssessment){ adapter = assessmentAdapter layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) - addItemDecoration(SpaceItemDecoration(context, 20)) + addItemDecoration(SideSpaceDecoration(context, 20, true)) PagerSnapHelper().attachToRecyclerView(this) } } @@ -165,6 +167,20 @@ class SelfAssessmentFragment : BaseFragment(R.lay } } + private val onAssessmentItemClickListener = object : OnItemClickListener { + override fun onItemClicked(position: Int, data: AssessmentItem) { + val type = when (position) { + 0 -> AssessmentResultFragment.AssessmentType.FACIAL + 1 -> AssessmentResultFragment.AssessmentType.SIDE_FACE + else -> AssessmentResultFragment.AssessmentType.TOOTH_BRUSH + } + + this@SelfAssessmentFragment.addFragmentToFullScreen( + CameraFragment.newInstance(type) + ) + } + } + private val onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { if(bind.clSelectAssessment.isVisible){ diff --git a/presentation/src/main/java/com/example/smiley/selfassessment/adapter/AssessmentItem.kt b/presentation/src/main/java/com/example/smiley/selfassessment/item/AssessmentItem.kt similarity index 74% rename from presentation/src/main/java/com/example/smiley/selfassessment/adapter/AssessmentItem.kt rename to presentation/src/main/java/com/example/smiley/selfassessment/item/AssessmentItem.kt index dca22401..f2bb7d35 100644 --- a/presentation/src/main/java/com/example/smiley/selfassessment/adapter/AssessmentItem.kt +++ b/presentation/src/main/java/com/example/smiley/selfassessment/item/AssessmentItem.kt @@ -1,4 +1,4 @@ -package com.example.smiley.selfassessment.adapter +package com.example.smiley.selfassessment.item import androidx.annotation.DrawableRes diff --git a/presentation/src/main/java/com/example/smiley/selfassessment/viewholder/AssessmentResultViewHolder.kt b/presentation/src/main/java/com/example/smiley/selfassessment/viewholder/AssessmentResultViewHolder.kt new file mode 100644 index 00000000..dcc7f244 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/selfassessment/viewholder/AssessmentResultViewHolder.kt @@ -0,0 +1,78 @@ +package com.example.smiley.selfassessment.viewholder + +import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.RecyclerView +import com.example.domain.assessment.model.Assessment +import com.example.smiley.databinding.LayoutAssessmentFaqBinding +import com.example.smiley.databinding.LayoutAssessmentHeaderBinding +import com.example.smiley.databinding.LayoutAssessmentRecommendVideoBinding +import com.example.smiley.databinding.LayoutAssessmentStatsBinding +import com.example.smiley.selfassessment.adapter.faq.ExpandFaqAdater +import com.example.smiley.selfassessment.adapter.recommendvideo.RecommendVideoAdater + +sealed class AssessmentResultViewHolder( + bind: ViewDataBinding +): RecyclerView.ViewHolder(bind.root){ + + abstract fun bind(item: Assessment) + + class Header ( + private val bind: LayoutAssessmentHeaderBinding + ): AssessmentResultViewHolder(bind){ + override fun bind(item: Assessment) { + val headerItem = item as? Assessment.Header + + with(bind){ + tvResultTag.text = headerItem?.resultTag + tvTitle.text = headerItem?.title + tvResultExplain.text = headerItem?.explain + } + } + } + + class Stats( + private val bind: LayoutAssessmentStatsBinding, + ): AssessmentResultViewHolder(bind){ + override fun bind(item: Assessment) { + val statsItem = item as? Assessment.Stats + + with(bind){ + pbFacialAsymmetry.progress = statsItem?.facialAsymmetry ?: 0 + pbEyeDegree.progress = statsItem?.eyeDegree ?: 0 + pbLipDegree.progress = statsItem?.lipDegree ?: 0 + + tvFacailPercent.text = "${pbFacialAsymmetry.progress}% / 100%" + tvEyeDegreePercent.text = "${pbEyeDegree.progress}% / 100%" + tvLipDegreePercent.text = "${pbLipDegree.progress}% / 100%" + } + } + } + + class Faq ( + private val bind: LayoutAssessmentFaqBinding, + ): AssessmentResultViewHolder(bind){ + override fun bind(item: Assessment) { + val faqList = (item as? Assessment.FaqList)?.faqList ?: arrayListOf() + + with(bind.rvFaq){ + adapter = ExpandFaqAdater(faqList) + setHasFixedSize(true) + isNestedScrollingEnabled = false + } + } + } + + class RecommendVideo ( + private val bind: LayoutAssessmentRecommendVideoBinding, + ): AssessmentResultViewHolder(bind){ + override fun bind(item: Assessment) { + val videoList = (item as? Assessment.RecommendVideoList)?.videos ?: arrayListOf() + + with(bind.rvRecommendVideo){ + adapter = RecommendVideoAdater(context, videoList) + setHasFixedSize(true) + isNestedScrollingEnabled = false + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/test_ai_assessment/FaceDetectViewModel.kt b/presentation/src/main/java/com/example/smiley/test_ai_assessment/FaceDetectViewModel.kt new file mode 100644 index 00000000..21ffb907 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/test_ai_assessment/FaceDetectViewModel.kt @@ -0,0 +1,65 @@ +package com.example.smiley.test_ai_assessment + +import android.graphics.Bitmap +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.example.domain.common.base.ResponseState +import com.example.domain.toothbrush.model.ToothBrush +import com.example.domain.toothbrush.usecase.CheckToothBrushStatusUseCase +import com.example.smiley.common.base.BaseStateFlowViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class FaceDetectViewModel @Inject constructor( + private val checkToothBrushStatusUseCase: CheckToothBrushStatusUseCase +): BaseStateFlowViewModel() { + + override fun initialState() = AssessmentState.Init + + /** + * 옆면 얼굴 감지 메소드 + */ + fun detectSideFace(userId:String, bitmap: Bitmap){ + } + + /** + * 칫솔모 교체 판별 메소드 + * @param userId String + * @param image ByteArray + */ + fun detectToothBrush(userId: String, image: ByteArray) { + viewModelScope.launch(Dispatchers.IO) { + checkToothBrushStatusUseCase(userId, image) + .onStart { setState(AssessmentState.Loading) } + .catch { + setState(AssessmentState.ShowToast(it.message.toString())) + Log.d("칫솔 검사 에러", it.message.toString()) + } + .collect { state -> + when (state) { + is ResponseState.Success -> { + setState(AssessmentState.ToothBrushResult(state.data)) + } + + is ResponseState.Error -> { + setState(AssessmentState.Error(state.error.message)) + } + } + } + } + } +} + +sealed class AssessmentState { + object Init: AssessmentState() + object Loading: AssessmentState() + data class ToothBrushResult(val result: ToothBrush): AssessmentState() + data class Error(val message: String): AssessmentState() + data class ShowToast(val message: String): AssessmentState() +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/test_ai_assessment/camera/CameraManager.kt b/presentation/src/main/java/com/example/smiley/test_ai_assessment/camera/CameraManager.kt new file mode 100644 index 00000000..cc594bf7 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/test_ai_assessment/camera/CameraManager.kt @@ -0,0 +1,152 @@ +package com.example.smiley.test_ai_assessment.camera + +import android.annotation.SuppressLint +import android.content.Context +import android.util.DisplayMetrics +import android.util.Log +import android.util.Size +import android.view.ScaleGestureDetector +import androidx.camera.core.* +import androidx.camera.core.ImageAnalysis.Analyzer +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class CameraManager( + private val context: Context, + private val lifecycleOwner: LifecycleOwner, + private val finderView: PreviewView, + private val graphicOverlay: GraphicOverlay, + private val analyzer: Analyzer? = null +) { + + companion object { + private const val TAG = "CameraXBasic" + } + + private var preview: Preview? = null + private var camera: Camera? = null + private var cameraProvider: ProcessCameraProvider? = null + private var imageAnalyzer: ImageAnalysis? = null + + + lateinit var cameraExecutor: ExecutorService + lateinit var imageCapture: ImageCapture + lateinit var metrics: DisplayMetrics + + private var cameraSelectorOption: Int = CameraSelector.LENS_FACING_BACK + var rotation: Float = 0f + + init { + createNewExecutor() + } + + private fun createNewExecutor() { + cameraExecutor = Executors.newSingleThreadExecutor() + } + + fun startCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + cameraProvider = cameraProviderFuture.get() + preview = Preview.Builder().build() + + /** + * ImageAnalzer로 FaceContourDetectionProcessor가 들어감 + * FaceCountourDetectionProcessor 안에 FaceContourGraphinc 클래스가 Canvas로 그려줌 + * Draw 메소드 안에서 Face 좌표 출력 가능 + * graphicOverlay를 인자로 전달해서 그 위에 그려줌 + */ + imageAnalyzer = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + analyzer?.run { + it.setAnalyzer(cameraExecutor,this) + } + } + + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(cameraSelectorOption) + .build() + + metrics = DisplayMetrics().also { finderView.display.getRealMetrics(it) } + imageCapture = ImageCapture.Builder() + .setTargetResolution(Size(metrics.widthPixels, metrics.heightPixels)) + .build() + + setUpPinchToZoom() + setCameraConfig(cameraProvider, cameraSelector) + }, ContextCompat.getMainExecutor(context)) + } + + fun stopCamera(){ + cameraProvider?.unbindAll() + } + + /** + * 카메라 구성 설정 + * 카메라를 실제로 바인딩하고 표시하는 부분 + * CameraX는 Lifecycle과 연동 가능 + */ + private fun setCameraConfig( + cameraProvider: ProcessCameraProvider?, + cameraSelector: CameraSelector + ) { + try { + cameraProvider?.unbindAll() + camera = cameraProvider?.bindToLifecycle( + lifecycleOwner, + cameraSelector, + preview, + imageCapture, + imageAnalyzer + ) + preview?.setSurfaceProvider( + finderView.surfaceProvider + ) + } catch (e: Exception) { + Log.e(TAG, "Use case binding failed", e) + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun setUpPinchToZoom() { + val listener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScale(detector: ScaleGestureDetector): Boolean { + val currentZoomRatio: Float = camera?.cameraInfo?.zoomState?.value?.zoomRatio ?: 1F + val delta = detector.scaleFactor + camera?.cameraControl?.setZoomRatio(currentZoomRatio * delta) + return true + } + } + val scaleGestureDetector = ScaleGestureDetector(context, listener) + finderView.setOnTouchListener { _, event -> + finderView.post { + scaleGestureDetector.onTouchEvent(event) + } + return@setOnTouchListener true + } + } + + fun changeCameraSelector() { + cameraProvider?.unbindAll() + cameraSelectorOption = + if (cameraSelectorOption == CameraSelector.LENS_FACING_BACK) CameraSelector.LENS_FACING_FRONT + else CameraSelector.LENS_FACING_BACK + graphicOverlay.toggleSelector() + + startCamera() + } + + fun isHorizontalMode() : Boolean { + return rotation == 90f || rotation == 270f + } + + fun isFrontMode() : Boolean { + return cameraSelectorOption == CameraSelector.LENS_FACING_FRONT + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/test_ai_assessment/camera/GraphicOverlay.kt b/presentation/src/main/java/com/example/smiley/test_ai_assessment/camera/GraphicOverlay.kt new file mode 100644 index 00000000..bc383a8b --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/test_ai_assessment/camera/GraphicOverlay.kt @@ -0,0 +1,143 @@ +package com.example.smiley.test_ai_assessment.camera + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import androidx.camera.core.CameraSelector +import kotlin.math.ceil + +open class GraphicOverlay(context: Context?, attrs: AttributeSet?) : + View(context, attrs) { + + private val lock = Any() + private val graphics: MutableList = ArrayList() + var mScale: Float? = null + var mOffsetX: Float? = null + var mOffsetY: Float? = null + var cameraSelector: Int = CameraSelector.LENS_FACING_BACK + lateinit var processBitmap: Bitmap + lateinit var processCanvas: Canvas + + abstract class Graphic(private val overlay: GraphicOverlay) { + + abstract fun draw(canvas: Canvas?) + + fun calculateRect(height: Float, width: Float, boundingBoxT: Rect): RectF { + + // for land scape + fun isLandScapeMode(): Boolean { + return overlay.context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + } + + fun whenLandScapeModeWidth(): Float { + return when(isLandScapeMode()) { + true -> width + false -> height + } + } + + fun whenLandScapeModeHeight(): Float { + return when(isLandScapeMode()) { + true -> height + false -> width + } + } + + val scaleX = overlay.width.toFloat() / whenLandScapeModeWidth() + val scaleY = overlay.height.toFloat() / whenLandScapeModeHeight() + val scale = scaleX.coerceAtLeast(scaleY) + overlay.mScale = scale + + // Calculate offset (we need to center the overlay on the target) + val offsetX = (overlay.width.toFloat() - ceil(whenLandScapeModeWidth() * scale)) / 2.0f + val offsetY = (overlay.height.toFloat() - ceil(whenLandScapeModeHeight() * scale)) / 2.0f + + overlay.mOffsetX = offsetX + overlay.mOffsetY = offsetY + + val mappedBox = RectF().apply { + left = boundingBoxT.right * scale + offsetX + top = boundingBoxT.top * scale + offsetY + right = boundingBoxT.left * scale + offsetX + bottom = boundingBoxT.bottom * scale + offsetY + } + + // for front mode + if (overlay.isFrontMode()) { + val centerX = overlay.width.toFloat() / 2 + mappedBox.apply { + left = centerX + (centerX - left) + right = centerX - (right - centerX) + } + } + return mappedBox + } + + fun translateX(horizontal: Float): Float { + return if (overlay.mScale != null && overlay.mOffsetX != null && !overlay.isFrontMode()) { + (horizontal * overlay.mScale!!) + overlay.mOffsetX!! + } else if (overlay.mScale != null && overlay.mOffsetX != null && overlay.isFrontMode()) { + val centerX = overlay.width.toFloat() / 2 + centerX - ((horizontal * overlay.mScale!!) + overlay.mOffsetX!! - centerX) + } else { + horizontal + } + } + + fun translateY(vertical: Float): Float { + return if (overlay.mScale != null && overlay.mOffsetY != null) { + (vertical * overlay.mScale!!) + overlay.mOffsetY!! + } else { + vertical + } + } + + } + + fun isFrontMode() = cameraSelector == CameraSelector.LENS_FACING_FRONT + + /** + * 카메라 전환 + */ + fun toggleSelector() { + cameraSelector = + if (cameraSelector == CameraSelector.LENS_FACING_BACK) CameraSelector.LENS_FACING_FRONT + else CameraSelector.LENS_FACING_BACK + } + + fun clear() { + synchronized(lock) { graphics.clear() } + postInvalidate() + } + + fun add(graphic: Graphic) { + synchronized(lock) { graphics.add(graphic) } + } + + fun remove(graphic: Graphic) { + synchronized(lock) { graphics.remove(graphic) } + postInvalidate() + } + + private fun initProcessCanvas () { + processBitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888) + processCanvas = Canvas(processBitmap) + } + + override fun onDraw(canvas: Canvas?) { + super.onDraw(canvas) + synchronized(lock) { + initProcessCanvas() + graphics.forEach { + it.draw(canvas) + it.draw(processCanvas) + } + } + } + +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/test_ai_assessment/camera/ImageCaptureManager.kt b/presentation/src/main/java/com/example/smiley/test_ai_assessment/camera/ImageCaptureManager.kt new file mode 100644 index 00000000..e7beb7d3 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/test_ai_assessment/camera/ImageCaptureManager.kt @@ -0,0 +1,5 @@ +package com.example.smiley.test_ai_assessment.camera + +class ImageCaptureManager { + +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/test_ai_assessment/mlkit/BaseImageAnalyzer.kt b/presentation/src/main/java/com/example/smiley/test_ai_assessment/mlkit/BaseImageAnalyzer.kt new file mode 100644 index 00000000..97b306f5 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/test_ai_assessment/mlkit/BaseImageAnalyzer.kt @@ -0,0 +1,51 @@ +package com.example.smiley.test_ai_assessment.mlkit + +import android.annotation.SuppressLint +import android.graphics.* +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import com.example.smiley.test_ai_assessment.camera.GraphicOverlay +import com.google.android.gms.tasks.Task +import com.google.mlkit.vision.common.InputImage + +abstract class BaseImageAnalyzer : ImageAnalysis.Analyzer { + + abstract val graphicOverlay: GraphicOverlay + + @SuppressLint("UnsafeExperimentalUsageError") + @androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class) + override fun analyze(imageProxy: ImageProxy) { + val mediaImage = imageProxy.image + mediaImage?.let { + detectInImage(InputImage.fromMediaImage(it, imageProxy.imageInfo.rotationDegrees)) + .addOnSuccessListener { results -> + onSuccess( + results, + graphicOverlay, + it.cropRect + ) + } + .addOnFailureListener { e -> + graphicOverlay.clear() + graphicOverlay.postInvalidate() + onFailure(e) + } + .addOnCompleteListener { + imageProxy.close() + } + } + } + + abstract fun stop() + + protected abstract fun detectInImage(image: InputImage): Task + + protected abstract fun onSuccess( + results: T, + graphicOverlay: GraphicOverlay, + rect: Rect + ) + + protected abstract fun onFailure(e: Exception) + +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/test_ai_assessment/mlkit/FaceContourDetectionProcessor.kt b/presentation/src/main/java/com/example/smiley/test_ai_assessment/mlkit/FaceContourDetectionProcessor.kt new file mode 100644 index 00000000..50d73a3e --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/test_ai_assessment/mlkit/FaceContourDetectionProcessor.kt @@ -0,0 +1,68 @@ +package com.example.smiley.test_ai_assessment.mlkit + +import android.graphics.Rect +import android.util.Log +import com.example.smiley.test_ai_assessment.camera.GraphicOverlay +import com.example.toothfairy.mlkit.FaceContourGraphic +import com.google.android.gms.tasks.Task +import com.google.mlkit.vision.common.InputImage +import com.google.mlkit.vision.face.Face +import com.google.mlkit.vision.face.FaceDetection +import com.google.mlkit.vision.face.FaceDetectorOptions +import java.io.IOException + +class FaceContourDetectionProcessor( + private val view: GraphicOverlay, + private val faceDetectResultCallback: (face: Face) -> Unit /* TODO 콜백으로 변경함 (이 콜백이 의미 없을 수도, 처리할 땐 필요할 수도 있음)*/ +) : BaseImageAnalyzer>() { + + /** + * 실시간으로 얼굴 감지해서 계속 찍음 (onSuccess 로그 찍어보기) + * 실시간으로 찍는거 말고 사진 찍었을 때만 찍게 변경하기 + */ + private val realTimeOpts = FaceDetectorOptions.Builder() + .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST) + .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL) + .build() + + private val detector = FaceDetection.getClient(realTimeOpts) + + override val graphicOverlay: GraphicOverlay + get() = view + + /** + * 얼굴 감지 실행 + */ + override fun detectInImage(image: InputImage): Task> { + return detector.process(image) + } + + override fun stop() { + try { + detector.close() + } catch (e: IOException) { + Log.e(TAG, "Exception thrown while trying to close Face Detector: $e") + } + } + + override fun onSuccess(results: List, graphicOverlay: GraphicOverlay, rect: Rect) { + graphicOverlay.clear() + results.forEach { + val faceGraphic = FaceContourGraphic(graphicOverlay, it, rect) + graphicOverlay.add(faceGraphic) + + // TODO 여기 콜백으로 바꾸기 + faceDetectResultCallback.invoke(it) + } + graphicOverlay.postInvalidate() + } + + override fun onFailure(e: Exception) { + Log.w(TAG, "Face Detector failed.$e") + } + + companion object { + private const val TAG = "FaceDetectorProcessor" + } + +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/test_ai_assessment/mlkit/FaceContourGraphic.kt b/presentation/src/main/java/com/example/smiley/test_ai_assessment/mlkit/FaceContourGraphic.kt new file mode 100644 index 00000000..cc97f074 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/test_ai_assessment/mlkit/FaceContourGraphic.kt @@ -0,0 +1,196 @@ +package com.example.toothfairy.mlkit + + +import android.graphics.* +import androidx.annotation.ColorInt +import com.example.smiley.test_ai_assessment.camera.GraphicOverlay +import com.google.mlkit.vision.face.Face +import com.google.mlkit.vision.face.FaceContour + + +class FaceContourGraphic( + overlay: GraphicOverlay, + private val face: Face, + private val imageRect: Rect +) : GraphicOverlay.Graphic(overlay) { + + companion object { + // 특징점의 크기 + private const val FACE_POSITION_RADIUS = 6.0f + private const val ID_TEXT_SIZE = 30.0f + private const val BOX_STROKE_WIDTH = 5.0f + } + + private val facePositionPaint: Paint + private val idPaint: Paint + private val boxPaint: Paint + + init { + val selectedColor = Color.WHITE + + facePositionPaint = Paint().apply { + color = selectedColor + } + + idPaint = Paint().apply { + color = selectedColor + textSize = ID_TEXT_SIZE + } + + boxPaint = Paint().apply { + color = selectedColor + style = Paint.Style.STROKE + strokeWidth = BOX_STROKE_WIDTH + } + } + + private fun Canvas.drawFace(facePosition: Int, @ColorInt selectedColor: Int) { + val contour = face.getContour(facePosition) + val path = Path() + contour?.points?.forEachIndexed { index, pointF -> + if (index == 0) { + path.moveTo( + translateX(pointF.x), + translateY(pointF.y) + ) + } + path.lineTo( + translateX(pointF.x), + translateY(pointF.y) + ) + } + val paint = Paint().apply { + color = selectedColor + style = Paint.Style.STROKE + strokeWidth = BOX_STROKE_WIDTH + } + drawPath(path, paint) + } + + /** + * 양쪽 눈 끝을 잇는 메소드 + * + * @param leftEyePosition Int + * @param rightEyePosition Int + * @param selectedColor Int + */ + private fun Canvas.drawFaceEyeLine(leftEyePosition: Int, rightEyePosition:Int, @ColorInt selectedColor: Int) { + val leftContour = face.getContour(leftEyePosition) + val rightContour = face.getContour(rightEyePosition) + val path = Path() + + val leftX = leftContour?.points?.get(0)?.x + val leftY = leftContour?.points?.get(0)?.y + + val rightX = rightContour?.points?.get(8)?.x + val rightY = rightContour?.points?.get(8)?.y + + if(leftX != null && leftY != null){ + path.moveTo( + translateX(leftX), + translateY(leftY) + ) + } + + if(rightX != null && rightY != null){ + path.lineTo( + translateX(rightX), + translateY(rightY) + ) + } + + drawPath(path, Paint().apply { + color = selectedColor + style = Paint.Style.STROKE + strokeWidth = BOX_STROKE_WIDTH + }) + } + + private fun Canvas.drawLipLine(lipPosition: Int, @ColorInt selectedColor: Int){ + val lip = face.getContour(lipPosition) + val path = Path() + + val leftX = lip?.points?.get(0)?.x + val leftY = lip?.points?.get(0)?.y + + val rightX = lip?.points?.get(10)?.x + val rightY = lip?.points?.get(10)?.y + + path.moveTo( + translateX(leftX!!), + translateY(leftY!!) + ) + + path.lineTo( + translateX(rightX!!), + translateY(rightY!!) + ) + + drawPath(path, Paint().apply { + color = selectedColor + style = Paint.Style.STROKE + strokeWidth = BOX_STROKE_WIDTH + }) + } + + override fun draw(canvas: Canvas?) { + + val rect = calculateRect( + imageRect.height().toFloat(), + imageRect.width().toFloat(), + face.boundingBox + ) + canvas?.drawRect(rect, boxPaint) + + val contours = face.allContours + + contours.forEach { + it.points.forEach { point -> + val px = translateX(point.x) + val py = translateY(point.y) + canvas?.drawCircle(px, py, FACE_POSITION_RADIUS, facePositionPaint) + } + } + + /** + * drawFace에서 해당 부위의 점들을 선으로 연결해줄 수있음 + * 선 색상 설정 가능 + * 처음 얼굴은 파란색 선으로 다 찍고 + * 반전 시킨 점들은 빨간색 선으로 찍거나 하면 좋을듯 + * + * drawFace로 비대칭 판별 라인도 그을 수 있을 듯 + * drawFace(FACE(왼쪽 눈 시작 점, 오른쪽 눈 끝 점),Color.RED) -> 양 쪽 눈 끝을 잇는 선이 그려짐 + */ + canvas?.run { + // face + drawFace(FaceContour.FACE, Color.WHITE) + + // left eye (왼쪽 눈 부분) + drawFace(FaceContour.LEFT_EYEBROW_TOP, Color.WHITE) // RED + drawFace(FaceContour.LEFT_EYE, Color.WHITE) // BLACK + drawFace(FaceContour.LEFT_EYEBROW_BOTTOM, Color.WHITE) // CYAN + + // right eye (오른쪽 눈 부분) + drawFace(FaceContour.RIGHT_EYE, Color.WHITE) // DKGRAY + drawFace(FaceContour.RIGHT_EYEBROW_BOTTOM, Color.WHITE) // GRAY + drawFace(FaceContour.RIGHT_EYEBROW_TOP, Color.WHITE) // GREEN + + // nose (코 부분) + drawFace(FaceContour.NOSE_BOTTOM, Color.WHITE) // LTGRAY + drawFace(FaceContour.NOSE_BRIDGE, Color.WHITE) // MAGENTA + + // rip (입술 부분) + drawFace(FaceContour.LOWER_LIP_BOTTOM, Color.WHITE) // WHITE + drawFace(FaceContour.LOWER_LIP_TOP, Color.WHITE) // YELLOW + drawFace(FaceContour.UPPER_LIP_BOTTOM, Color.WHITE) // GREEN + drawFace(FaceContour.UPPER_LIP_TOP, Color.WHITE) // CYAN + + // 눈 양 끝 점 잇기 + drawFaceEyeLine(FaceContour.LEFT_EYE, FaceContour.RIGHT_EYE, Color.BLUE) + drawFaceEyeLine(FaceContour.LEFT_EYE, FaceContour.RIGHT_EYE, Color.BLUE) + + // 입술 라인 그리기 + drawLipLine(FaceContour.UPPER_LIP_TOP, Color.BLUE) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/test_ai_assessment/util/BitmapExt.kt b/presentation/src/main/java/com/example/smiley/test_ai_assessment/util/BitmapExt.kt new file mode 100644 index 00000000..3244f3ab --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/test_ai_assessment/util/BitmapExt.kt @@ -0,0 +1,93 @@ +package com.example.smiley.test_ai_assessment.util + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.net.Uri +import android.util.Log +import android.view.View +import com.example.smiley.test_ai_assessment.camera.GraphicOverlay +import java.io.FileOutputStream + + +interface BitmapSavedCallback{ + fun onSaved(path:String) +} + +/** + * 비트맵을 캔버스에 그리는 메소드 + * @param overlay GraphicOverlay + * @param bitmap Bitmap + * @param isHorizontalMode Boolean + */ +fun Bitmap.drawBitmapOnCanvas(overlay: GraphicOverlay, isHorizontalMode: Boolean) { + val canvas = overlay.processCanvas + val paint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OVER) } + + canvas.drawBitmap( + this, + 0f, + this.getBaseYByView(overlay, isHorizontalMode), + paint + ) +} + +/** + * Bitmap을 갤러리에 저장한 후 해당 경로를 ViewModel에 저장 + * 결과 화면에서 경로 변수에 Observer 걸어서 감지 + */ +fun Bitmap.saveToGallery(context: Context, saveCallback: BitmapSavedCallback) { + val path = makeTempFile().apply { + FileOutputStream(this).run { + this@saveToGallery.compress(Bitmap.CompressFormat.JPEG, 100, this) + flush() + close() + } + Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).also { + it.data = Uri.fromFile(this) + context.sendBroadcast(it) + } + }.path + + saveCallback.onSaved(path) + + Log.d("사진 경로", path) +} + + +fun Bitmap.rotateFlipImage(degree: Float, isFrontMode: Boolean): Bitmap? { + val realRotation = when (degree) { + 0f -> 90f + 90f -> 0f + 180f -> 270f + else -> 180f + } + val matrix = Matrix().apply { + if (isFrontMode) { + preScale(-1.0f, 1.0f) + } + postRotate(realRotation) + } + return Bitmap.createBitmap(this, 0, 0, width, height, matrix, false) +} + +fun Bitmap.scaleImage(view: View, isHorizontalRotation: Boolean): Bitmap? { + val ratio = view.width.toFloat() / view.height.toFloat() + val newHeight = (view.width * ratio).toInt() + + return when (isHorizontalRotation) { + true -> Bitmap.createScaledBitmap(this, view.width, newHeight, false) + false -> Bitmap.createScaledBitmap(this, view.width, view.height, false) + } +} + +fun Bitmap.getBaseYByView(view: View, isHorizontalRotation: Boolean): Float { + return when (isHorizontalRotation) { + true -> (view.height.toFloat() / 2) - (this.height.toFloat() / 2) + false -> 0f + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/example/smiley/test_ai_assessment/util/imageToBitmap.kt b/presentation/src/main/java/com/example/smiley/test_ai_assessment/util/imageToBitmap.kt new file mode 100644 index 00000000..e54d3975 --- /dev/null +++ b/presentation/src/main/java/com/example/smiley/test_ai_assessment/util/imageToBitmap.kt @@ -0,0 +1,29 @@ +package com.example.smiley.test_ai_assessment.util + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.Image +import android.os.Environment +import java.io.File + +val rootFolder = + File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), + "Smiley_${File.separator}" + ).apply { + if (!exists()) + mkdirs() + } + +fun makeTempFile(): File = File.createTempFile( + "${System.currentTimeMillis()}", + ".png", + rootFolder +) + +fun Image.imageToBitmap(): Bitmap? { + val buffer = this.planes[0].buffer + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, null) +} \ No newline at end of file diff --git a/presentation/src/main/res/drawable-hdpi/first_on_boarding_logo.png b/presentation/src/main/res/drawable-hdpi/first_on_boarding_logo.png deleted file mode 100644 index c9390cfa..00000000 Binary files a/presentation/src/main/res/drawable-hdpi/first_on_boarding_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-hdpi/ic_chat_bot.png b/presentation/src/main/res/drawable-hdpi/ic_chat_bot.png deleted file mode 100644 index c348c26d..00000000 Binary files a/presentation/src/main/res/drawable-hdpi/ic_chat_bot.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-hdpi/ic_location_img.png b/presentation/src/main/res/drawable-hdpi/ic_location_img.png deleted file mode 100644 index 5d970511..00000000 Binary files a/presentation/src/main/res/drawable-hdpi/ic_location_img.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-hdpi/img_background_black_trans_50.png b/presentation/src/main/res/drawable-hdpi/img_background_black_trans_50.png deleted file mode 100644 index 8de4e1aa..00000000 Binary files a/presentation/src/main/res/drawable-hdpi/img_background_black_trans_50.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-hdpi/img_banner_ai.png b/presentation/src/main/res/drawable-hdpi/img_banner_ai.png new file mode 100644 index 00000000..fd7fc119 Binary files /dev/null and b/presentation/src/main/res/drawable-hdpi/img_banner_ai.png differ diff --git a/presentation/src/main/res/drawable-hdpi/img_banner_medicine.png b/presentation/src/main/res/drawable-hdpi/img_banner_medicine.png new file mode 100644 index 00000000..2a6009db Binary files /dev/null and b/presentation/src/main/res/drawable-hdpi/img_banner_medicine.png differ diff --git a/presentation/src/main/res/drawable-hdpi/img_camera_btn.png b/presentation/src/main/res/drawable-hdpi/img_camera_btn.png new file mode 100644 index 00000000..2457fd48 Binary files /dev/null and b/presentation/src/main/res/drawable-hdpi/img_camera_btn.png differ diff --git a/presentation/src/main/res/drawable-hdpi/img_facial_youtube_thumbnail_1.png b/presentation/src/main/res/drawable-hdpi/img_facial_youtube_thumbnail_1.png new file mode 100644 index 00000000..93e57b1c Binary files /dev/null and b/presentation/src/main/res/drawable-hdpi/img_facial_youtube_thumbnail_1.png differ diff --git a/presentation/src/main/res/drawable-hdpi/img_facial_youtube_thumbnail_2.png b/presentation/src/main/res/drawable-hdpi/img_facial_youtube_thumbnail_2.png new file mode 100644 index 00000000..5d1b1972 Binary files /dev/null and b/presentation/src/main/res/drawable-hdpi/img_facial_youtube_thumbnail_2.png differ diff --git a/presentation/src/main/res/drawable-hdpi/img_facial_youtube_thumbnail_3.png b/presentation/src/main/res/drawable-hdpi/img_facial_youtube_thumbnail_3.png new file mode 100644 index 00000000..9a114ef3 Binary files /dev/null and b/presentation/src/main/res/drawable-hdpi/img_facial_youtube_thumbnail_3.png differ diff --git a/presentation/src/main/res/drawable-hdpi/img_guide_square.png b/presentation/src/main/res/drawable-hdpi/img_guide_square.png new file mode 100644 index 00000000..ccc11e8e Binary files /dev/null and b/presentation/src/main/res/drawable-hdpi/img_guide_square.png differ diff --git a/presentation/src/main/res/drawable-hdpi/login_logo.png b/presentation/src/main/res/drawable-hdpi/login_logo.png deleted file mode 100644 index b860aafd..00000000 Binary files a/presentation/src/main/res/drawable-hdpi/login_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-hdpi/second_on_boarding_logo.png b/presentation/src/main/res/drawable-hdpi/second_on_boarding_logo.png deleted file mode 100644 index 9e206100..00000000 Binary files a/presentation/src/main/res/drawable-hdpi/second_on_boarding_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-mdpi/first_on_boarding_logo.png b/presentation/src/main/res/drawable-mdpi/first_on_boarding_logo.png deleted file mode 100644 index a4300a6b..00000000 Binary files a/presentation/src/main/res/drawable-mdpi/first_on_boarding_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-mdpi/ic_chat_bot.png b/presentation/src/main/res/drawable-mdpi/ic_chat_bot.png deleted file mode 100644 index e9a63c53..00000000 Binary files a/presentation/src/main/res/drawable-mdpi/ic_chat_bot.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-mdpi/ic_location_img.png b/presentation/src/main/res/drawable-mdpi/ic_location_img.png deleted file mode 100644 index a5dd458b..00000000 Binary files a/presentation/src/main/res/drawable-mdpi/ic_location_img.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-mdpi/img_background_black_trans_50.png b/presentation/src/main/res/drawable-mdpi/img_background_black_trans_50.png deleted file mode 100644 index 860ccd28..00000000 Binary files a/presentation/src/main/res/drawable-mdpi/img_background_black_trans_50.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-mdpi/img_banner_ai.png b/presentation/src/main/res/drawable-mdpi/img_banner_ai.png new file mode 100644 index 00000000..f1dc0cb2 Binary files /dev/null and b/presentation/src/main/res/drawable-mdpi/img_banner_ai.png differ diff --git a/presentation/src/main/res/drawable-mdpi/img_banner_medicine.png b/presentation/src/main/res/drawable-mdpi/img_banner_medicine.png new file mode 100644 index 00000000..792fcc79 Binary files /dev/null and b/presentation/src/main/res/drawable-mdpi/img_banner_medicine.png differ diff --git a/presentation/src/main/res/drawable-mdpi/img_camera_btn.png b/presentation/src/main/res/drawable-mdpi/img_camera_btn.png new file mode 100644 index 00000000..2cf33341 Binary files /dev/null and b/presentation/src/main/res/drawable-mdpi/img_camera_btn.png differ diff --git a/presentation/src/main/res/drawable-mdpi/img_facial_youtube_thumbnail_1.png b/presentation/src/main/res/drawable-mdpi/img_facial_youtube_thumbnail_1.png new file mode 100644 index 00000000..d05c424f Binary files /dev/null and b/presentation/src/main/res/drawable-mdpi/img_facial_youtube_thumbnail_1.png differ diff --git a/presentation/src/main/res/drawable-mdpi/img_facial_youtube_thumbnail_2.png b/presentation/src/main/res/drawable-mdpi/img_facial_youtube_thumbnail_2.png new file mode 100644 index 00000000..074b38ac Binary files /dev/null and b/presentation/src/main/res/drawable-mdpi/img_facial_youtube_thumbnail_2.png differ diff --git a/presentation/src/main/res/drawable-mdpi/img_facial_youtube_thumbnail_3.png b/presentation/src/main/res/drawable-mdpi/img_facial_youtube_thumbnail_3.png new file mode 100644 index 00000000..cf5c5e05 Binary files /dev/null and b/presentation/src/main/res/drawable-mdpi/img_facial_youtube_thumbnail_3.png differ diff --git a/presentation/src/main/res/drawable-mdpi/img_guide_square.png b/presentation/src/main/res/drawable-mdpi/img_guide_square.png new file mode 100644 index 00000000..46bf7745 Binary files /dev/null and b/presentation/src/main/res/drawable-mdpi/img_guide_square.png differ diff --git a/presentation/src/main/res/drawable-mdpi/login_logo.png b/presentation/src/main/res/drawable-mdpi/login_logo.png deleted file mode 100644 index c6477162..00000000 Binary files a/presentation/src/main/res/drawable-mdpi/login_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-mdpi/second_on_boarding_logo.png b/presentation/src/main/res/drawable-mdpi/second_on_boarding_logo.png deleted file mode 100644 index 0688d37b..00000000 Binary files a/presentation/src/main/res/drawable-mdpi/second_on_boarding_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xhdpi/first_on_boarding_logo.png b/presentation/src/main/res/drawable-xhdpi/first_on_boarding_logo.png deleted file mode 100644 index ae042ad9..00000000 Binary files a/presentation/src/main/res/drawable-xhdpi/first_on_boarding_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xhdpi/ic_chat_bot.png b/presentation/src/main/res/drawable-xhdpi/ic_chat_bot.png deleted file mode 100644 index f5e2bd08..00000000 Binary files a/presentation/src/main/res/drawable-xhdpi/ic_chat_bot.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xhdpi/ic_location_img.png b/presentation/src/main/res/drawable-xhdpi/ic_location_img.png deleted file mode 100644 index e6aa9c5f..00000000 Binary files a/presentation/src/main/res/drawable-xhdpi/ic_location_img.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xhdpi/img_background_black_trans_50.png b/presentation/src/main/res/drawable-xhdpi/img_background_black_trans_50.png deleted file mode 100644 index 9a94bd66..00000000 Binary files a/presentation/src/main/res/drawable-xhdpi/img_background_black_trans_50.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xhdpi/img_banner_ai.png b/presentation/src/main/res/drawable-xhdpi/img_banner_ai.png new file mode 100644 index 00000000..df5b8c4c Binary files /dev/null and b/presentation/src/main/res/drawable-xhdpi/img_banner_ai.png differ diff --git a/presentation/src/main/res/drawable-xhdpi/img_banner_medicine.png b/presentation/src/main/res/drawable-xhdpi/img_banner_medicine.png new file mode 100644 index 00000000..088c9bad Binary files /dev/null and b/presentation/src/main/res/drawable-xhdpi/img_banner_medicine.png differ diff --git a/presentation/src/main/res/drawable-xhdpi/img_camera_btn.png b/presentation/src/main/res/drawable-xhdpi/img_camera_btn.png new file mode 100644 index 00000000..cb32259f Binary files /dev/null and b/presentation/src/main/res/drawable-xhdpi/img_camera_btn.png differ diff --git a/presentation/src/main/res/drawable-xhdpi/img_facial_youtube_thumbnail_1.png b/presentation/src/main/res/drawable-xhdpi/img_facial_youtube_thumbnail_1.png new file mode 100644 index 00000000..9ab86b83 Binary files /dev/null and b/presentation/src/main/res/drawable-xhdpi/img_facial_youtube_thumbnail_1.png differ diff --git a/presentation/src/main/res/drawable-xhdpi/img_facial_youtube_thumbnail_2.png b/presentation/src/main/res/drawable-xhdpi/img_facial_youtube_thumbnail_2.png new file mode 100644 index 00000000..1976d8b1 Binary files /dev/null and b/presentation/src/main/res/drawable-xhdpi/img_facial_youtube_thumbnail_2.png differ diff --git a/presentation/src/main/res/drawable-xhdpi/img_facial_youtube_thumbnail_3.png b/presentation/src/main/res/drawable-xhdpi/img_facial_youtube_thumbnail_3.png new file mode 100644 index 00000000..ae813af2 Binary files /dev/null and b/presentation/src/main/res/drawable-xhdpi/img_facial_youtube_thumbnail_3.png differ diff --git a/presentation/src/main/res/drawable-xhdpi/img_guide_square.png b/presentation/src/main/res/drawable-xhdpi/img_guide_square.png new file mode 100644 index 00000000..1e8ced9b Binary files /dev/null and b/presentation/src/main/res/drawable-xhdpi/img_guide_square.png differ diff --git a/presentation/src/main/res/drawable-xhdpi/login_logo.png b/presentation/src/main/res/drawable-xhdpi/login_logo.png deleted file mode 100644 index 037e2df4..00000000 Binary files a/presentation/src/main/res/drawable-xhdpi/login_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xhdpi/second_on_boarding_logo.png b/presentation/src/main/res/drawable-xhdpi/second_on_boarding_logo.png deleted file mode 100644 index 1a009d22..00000000 Binary files a/presentation/src/main/res/drawable-xhdpi/second_on_boarding_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxhdpi/first_on_boarding_logo.png b/presentation/src/main/res/drawable-xxhdpi/first_on_boarding_logo.png deleted file mode 100644 index 50ef118f..00000000 Binary files a/presentation/src/main/res/drawable-xxhdpi/first_on_boarding_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxhdpi/ic_chat_bot.png b/presentation/src/main/res/drawable-xxhdpi/ic_chat_bot.png deleted file mode 100644 index cfb35082..00000000 Binary files a/presentation/src/main/res/drawable-xxhdpi/ic_chat_bot.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxhdpi/ic_location_img.png b/presentation/src/main/res/drawable-xxhdpi/ic_location_img.png deleted file mode 100644 index 0c8dfa6d..00000000 Binary files a/presentation/src/main/res/drawable-xxhdpi/ic_location_img.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxhdpi/img_background_black_trans_50.png b/presentation/src/main/res/drawable-xxhdpi/img_background_black_trans_50.png deleted file mode 100644 index ddfe84ca..00000000 Binary files a/presentation/src/main/res/drawable-xxhdpi/img_background_black_trans_50.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxhdpi/img_banner_ai.png b/presentation/src/main/res/drawable-xxhdpi/img_banner_ai.png new file mode 100644 index 00000000..046c4609 Binary files /dev/null and b/presentation/src/main/res/drawable-xxhdpi/img_banner_ai.png differ diff --git a/presentation/src/main/res/drawable-xxhdpi/img_banner_medicine.png b/presentation/src/main/res/drawable-xxhdpi/img_banner_medicine.png new file mode 100644 index 00000000..30452ef5 Binary files /dev/null and b/presentation/src/main/res/drawable-xxhdpi/img_banner_medicine.png differ diff --git a/presentation/src/main/res/drawable-xxhdpi/img_camera_btn.png b/presentation/src/main/res/drawable-xxhdpi/img_camera_btn.png new file mode 100644 index 00000000..b2d15a2e Binary files /dev/null and b/presentation/src/main/res/drawable-xxhdpi/img_camera_btn.png differ diff --git a/presentation/src/main/res/drawable-xxhdpi/img_facial_youtube_thumbnail_1.png b/presentation/src/main/res/drawable-xxhdpi/img_facial_youtube_thumbnail_1.png new file mode 100644 index 00000000..093c7bc9 Binary files /dev/null and b/presentation/src/main/res/drawable-xxhdpi/img_facial_youtube_thumbnail_1.png differ diff --git a/presentation/src/main/res/drawable-xxhdpi/img_facial_youtube_thumbnail_2.png b/presentation/src/main/res/drawable-xxhdpi/img_facial_youtube_thumbnail_2.png new file mode 100644 index 00000000..a523bc80 Binary files /dev/null and b/presentation/src/main/res/drawable-xxhdpi/img_facial_youtube_thumbnail_2.png differ diff --git a/presentation/src/main/res/drawable-xxhdpi/img_facial_youtube_thumbnail_3.png b/presentation/src/main/res/drawable-xxhdpi/img_facial_youtube_thumbnail_3.png new file mode 100644 index 00000000..35c4d28c Binary files /dev/null and b/presentation/src/main/res/drawable-xxhdpi/img_facial_youtube_thumbnail_3.png differ diff --git a/presentation/src/main/res/drawable-xxhdpi/img_guide_square.png b/presentation/src/main/res/drawable-xxhdpi/img_guide_square.png new file mode 100644 index 00000000..30c956b1 Binary files /dev/null and b/presentation/src/main/res/drawable-xxhdpi/img_guide_square.png differ diff --git a/presentation/src/main/res/drawable-xxhdpi/img_mock_profile.png b/presentation/src/main/res/drawable-xxhdpi/img_mock_profile.png new file mode 100644 index 00000000..28f4c6a7 Binary files /dev/null and b/presentation/src/main/res/drawable-xxhdpi/img_mock_profile.png differ diff --git a/presentation/src/main/res/drawable-xxhdpi/login_logo.png b/presentation/src/main/res/drawable-xxhdpi/login_logo.png deleted file mode 100644 index 90786485..00000000 Binary files a/presentation/src/main/res/drawable-xxhdpi/login_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxhdpi/second_on_boarding_logo.png b/presentation/src/main/res/drawable-xxhdpi/second_on_boarding_logo.png deleted file mode 100644 index 9b31a15a..00000000 Binary files a/presentation/src/main/res/drawable-xxhdpi/second_on_boarding_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/first_on_boarding_logo.png b/presentation/src/main/res/drawable-xxxhdpi/first_on_boarding_logo.png deleted file mode 100644 index fcfba09d..00000000 Binary files a/presentation/src/main/res/drawable-xxxhdpi/first_on_boarding_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/ic_chat_bot.png b/presentation/src/main/res/drawable-xxxhdpi/ic_chat_bot.png deleted file mode 100644 index dbdf8740..00000000 Binary files a/presentation/src/main/res/drawable-xxxhdpi/ic_chat_bot.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/ic_location_img.png b/presentation/src/main/res/drawable-xxxhdpi/ic_location_img.png deleted file mode 100644 index 3f1073db..00000000 Binary files a/presentation/src/main/res/drawable-xxxhdpi/ic_location_img.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/img_background_black_trans_50.png b/presentation/src/main/res/drawable-xxxhdpi/img_background_black_trans_50.png deleted file mode 100644 index c7b58a43..00000000 Binary files a/presentation/src/main/res/drawable-xxxhdpi/img_background_black_trans_50.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/img_banner_ai.png b/presentation/src/main/res/drawable-xxxhdpi/img_banner_ai.png new file mode 100644 index 00000000..9b326563 Binary files /dev/null and b/presentation/src/main/res/drawable-xxxhdpi/img_banner_ai.png differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/img_banner_medicine.png b/presentation/src/main/res/drawable-xxxhdpi/img_banner_medicine.png new file mode 100644 index 00000000..c8d3237d Binary files /dev/null and b/presentation/src/main/res/drawable-xxxhdpi/img_banner_medicine.png differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/img_camera_btn.png b/presentation/src/main/res/drawable-xxxhdpi/img_camera_btn.png new file mode 100644 index 00000000..8f43b0ce Binary files /dev/null and b/presentation/src/main/res/drawable-xxxhdpi/img_camera_btn.png differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/img_facial_youtube_thumbnail_1.png b/presentation/src/main/res/drawable-xxxhdpi/img_facial_youtube_thumbnail_1.png new file mode 100644 index 00000000..64626747 Binary files /dev/null and b/presentation/src/main/res/drawable-xxxhdpi/img_facial_youtube_thumbnail_1.png differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/img_facial_youtube_thumbnail_2.png b/presentation/src/main/res/drawable-xxxhdpi/img_facial_youtube_thumbnail_2.png new file mode 100644 index 00000000..1cb2d1b6 Binary files /dev/null and b/presentation/src/main/res/drawable-xxxhdpi/img_facial_youtube_thumbnail_2.png differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/img_facial_youtube_thumbnail_3.png b/presentation/src/main/res/drawable-xxxhdpi/img_facial_youtube_thumbnail_3.png new file mode 100644 index 00000000..d4f10892 Binary files /dev/null and b/presentation/src/main/res/drawable-xxxhdpi/img_facial_youtube_thumbnail_3.png differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/img_guide_square.png b/presentation/src/main/res/drawable-xxxhdpi/img_guide_square.png new file mode 100644 index 00000000..15efac6e Binary files /dev/null and b/presentation/src/main/res/drawable-xxxhdpi/img_guide_square.png differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/img_mock_profile.png b/presentation/src/main/res/drawable-xxxhdpi/img_mock_profile.png new file mode 100644 index 00000000..60ea9957 Binary files /dev/null and b/presentation/src/main/res/drawable-xxxhdpi/img_mock_profile.png differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/login_logo.png b/presentation/src/main/res/drawable-xxxhdpi/login_logo.png deleted file mode 100644 index 0bd60715..00000000 Binary files a/presentation/src/main/res/drawable-xxxhdpi/login_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable-xxxhdpi/second_on_boarding_logo.png b/presentation/src/main/res/drawable-xxxhdpi/second_on_boarding_logo.png deleted file mode 100644 index 39bcfd0e..00000000 Binary files a/presentation/src/main/res/drawable-xxxhdpi/second_on_boarding_logo.png and /dev/null differ diff --git a/presentation/src/main/res/drawable/background_progress_bar.xml b/presentation/src/main/res/drawable/background_progress_bar.xml index 11f5e9ab..0fc32893 100644 --- a/presentation/src/main/res/drawable/background_progress_bar.xml +++ b/presentation/src/main/res/drawable/background_progress_bar.xml @@ -2,23 +2,16 @@ - - + + - - - + + diff --git a/presentation/src/main/res/drawable/ic_arrow_down.xml b/presentation/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 00000000..c141ca7f --- /dev/null +++ b/presentation/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,13 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_blue_circle_check.xml b/presentation/src/main/res/drawable/ic_blue_circle_check.xml new file mode 100644 index 00000000..25383ced --- /dev/null +++ b/presentation/src/main/res/drawable/ic_blue_circle_check.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/presentation/src/main/res/drawable/ic_call.xml b/presentation/src/main/res/drawable/ic_call.xml index adb64400..ead4ee3f 100644 --- a/presentation/src/main/res/drawable/ic_call.xml +++ b/presentation/src/main/res/drawable/ic_call.xml @@ -7,17 +7,17 @@ android:pathData="M19.062,20.969C17.108,22.924 12.103,21.087 7.883,16.868C3.664,12.648 1.827,7.643 3.782,5.689L5.068,4.402C5.957,3.514 7.421,3.537 8.338,4.455L10.331,6.448C11.248,7.365 11.272,8.829 10.383,9.717L10.107,9.993C9.627,10.473 9.58,11.247 10.026,11.787C10.456,12.308 10.919,12.827 11.422,13.329C11.924,13.832 12.443,14.295 12.964,14.725C13.504,15.17 14.278,15.124 14.757,14.644L15.034,14.368C15.922,13.479 17.386,13.503 18.303,14.42L20.296,16.413C21.214,17.33 21.237,18.794 20.349,19.683L19.062,20.969Z" android:strokeWidth="1.5" android:fillColor="#00000000" - android:strokeColor="#8E8E8E"/> + android:strokeColor="#424958"/> diff --git a/presentation/src/main/res/drawable/ic_call_saved.xml b/presentation/src/main/res/drawable/ic_call_saved.xml new file mode 100644 index 00000000..adb64400 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_call_saved.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_grid.xml b/presentation/src/main/res/drawable/ic_grid.xml new file mode 100644 index 00000000..bbb15363 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_grid.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/presentation/src/main/res/drawable/ic_home.xml b/presentation/src/main/res/drawable/ic_home.xml index 5bdf8fcd..3e1d24ef 100644 --- a/presentation/src/main/res/drawable/ic_home.xml +++ b/presentation/src/main/res/drawable/ic_home.xml @@ -4,8 +4,9 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:strokeColor="#424958" + android:strokeLineCap="round"/> diff --git a/presentation/src/main/res/drawable/ic_home_saved.xml b/presentation/src/main/res/drawable/ic_home_saved.xml new file mode 100644 index 00000000..5bdf8fcd --- /dev/null +++ b/presentation/src/main/res/drawable/ic_home_saved.xml @@ -0,0 +1,11 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_map.xml b/presentation/src/main/res/drawable/ic_map.xml new file mode 100644 index 00000000..b4776dac --- /dev/null +++ b/presentation/src/main/res/drawable/ic_map.xml @@ -0,0 +1,13 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_office_circle_graph.xml b/presentation/src/main/res/drawable/ic_office_circle_graph.xml new file mode 100644 index 00000000..05c1b5bf --- /dev/null +++ b/presentation/src/main/res/drawable/ic_office_circle_graph.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_office_heart.xml b/presentation/src/main/res/drawable/ic_office_heart.xml new file mode 100644 index 00000000..16ce22bf --- /dev/null +++ b/presentation/src/main/res/drawable/ic_office_heart.xml @@ -0,0 +1,9 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_office_setting.xml b/presentation/src/main/res/drawable/ic_office_setting.xml new file mode 100644 index 00000000..ccfe257d --- /dev/null +++ b/presentation/src/main/res/drawable/ic_office_setting.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_quote.xml b/presentation/src/main/res/drawable/ic_quote.xml new file mode 100644 index 00000000..d4f50102 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_quote.xml @@ -0,0 +1,13 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_refresh.xml b/presentation/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 00000000..9891572a --- /dev/null +++ b/presentation/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,13 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_stats.xml b/presentation/src/main/res/drawable/ic_stats.xml new file mode 100644 index 00000000..a0d9800a --- /dev/null +++ b/presentation/src/main/res/drawable/ic_stats.xml @@ -0,0 +1,13 @@ + + + diff --git a/presentation/src/main/res/drawable/img_tooth_brush.xml b/presentation/src/main/res/drawable/img_tooth_brush.xml new file mode 100644 index 00000000..5f16b8bb --- /dev/null +++ b/presentation/src/main/res/drawable/img_tooth_brush.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/presentation/src/main/res/drawable/shadow_top_corner_radius_15.xml b/presentation/src/main/res/drawable/shadow_top_corner_radius_15.xml new file mode 100644 index 00000000..0bf1b483 --- /dev/null +++ b/presentation/src/main/res/drawable/shadow_top_corner_radius_15.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_assessment_result.xml b/presentation/src/main/res/layout/fragment_assessment_result.xml new file mode 100644 index 00000000..b0c5b903 --- /dev/null +++ b/presentation/src/main/res/layout/fragment_assessment_result.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_camera.xml b/presentation/src/main/res/layout/fragment_camera.xml new file mode 100644 index 00000000..396fc443 --- /dev/null +++ b/presentation/src/main/res/layout/fragment_camera.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/layout/fragment_home.xml b/presentation/src/main/res/layout/fragment_home.xml index 04f51e3b..e899f92d 100644 --- a/presentation/src/main/res/layout/fragment_home.xml +++ b/presentation/src/main/res/layout/fragment_home.xml @@ -4,731 +4,620 @@ xmlns:tools="http://schemas.android.com/tools" tools:context=".main.home.HomeFragment"> - - - + android:layout_height="match_parent"> + + - + - + android:paddingBottom="100dp"> - - - - - - + android:layout_marginStart="@dimen/side_margin" - - + app:layout_constraintBottom_toTopOf="@id/tvToday" + app:layout_constraintStart_toEndOf="@id/ivProfile" + app:layout_constraintTop_toTopOf="@id/ivProfile" /> + app:layout_constraintBottom_toBottomOf="@id/ivProfile" + app:layout_constraintStart_toStartOf="@id/tvNickname" + app:layout_constraintTop_toBottomOf="@id/tvNickname" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintBottom_toBottomOf="@id/tvToday" + app:layout_constraintStart_toEndOf="@id/tvToday" + app:layout_constraintTop_toTopOf="@id/tvToday" /> - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/ivProfile"/> + - - - - - - + android:background="@color/transparent" + android:fillViewport="true" + android:overScrollMode="never" + android:scrollbars="none" + app:behavior_hideable="false" + app:behavior_peekHeight="530dp" + app:layout_behavior="@string/bottom_sheet_behavior"> - + android:background="@drawable/shadow_top_corner_radius_15" + android:clipChildren="false" + android:clipToPadding="false" + android:elevation="1dp"> - + + + - - - - - - - - - - + + + + + + - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/rv_famous_hospital"/> + + app:layout_constraintStart_toStartOf="@id/clPhrase" + app:layout_constraintTop_toBottomOf="@id/clPhrase" /> - - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@+id/tv_find_near_by_hosptial"/> - - + - - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/cl_famous_hospital_layout"> + + + + + + + + + + + - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"/> + + + + + - - app:layout_constraintStart_toEndOf="@id/tv_recommend_video" - app:layout_constraintTop_toTopOf="@id/tv_recommend_video" - app:layout_constraintBottom_toBottomOf="@id/tv_recommend_video"/> + - + - - - - + + + + + + + + + + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/clDentalMagazine"> - + android:fontFamily="@font/pretendard_extrabold" + android:includeFontPadding="false" + android:text="내 주변 인기치과" + android:textColor="@color/black1_20" + android:textSize="@dimen/H4_M500_18_auto" - - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - - + app:layout_constraintBottom_toBottomOf="@id/tv_famous_dental_hospital" + app:layout_constraintStart_toEndOf="@id/tv_famous_dental_hospital" + app:layout_constraintTop_toTopOf="@id/tv_famous_dental_hospital" /> - - android:text="오늘의 매거진" - android:textSize="@dimen/H4_M500_18_auto" - android:textColor="@color/black1_20" - android:fontFamily="@font/pretendard_extrabold" - android:includeFontPadding="false" - android:clickable="true" + - app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"/> + - - app:layout_constraintStart_toEndOf="@id/tv_magazine_title" - app:layout_constraintTop_toTopOf="@id/tv_magazine_title" - app:layout_constraintBottom_toBottomOf="@id/tv_magazine_title"/> + + + - + app:layout_constraintBottom_toBottomOf="@id/sfl_partner_shimmer" + app:layout_constraintEnd_toEndOf="@id/sfl_partner_shimmer" + app:layout_constraintStart_toStartOf="@id/sfl_partner_shimmer" + app:layout_constraintTop_toTopOf="@id/sfl_partner_shimmer" /> - + + + + + + + + + + android:layout_marginTop="10dp" + android:paddingVertical="@dimen/side_margin_2x" + android:background="@color/white" - - - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/cl_famous_hospital_layout"> - - - - - - - - - - + + + + + - - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/cl_magazine_layout"> + - + - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" /> + - - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/view_divider"/> + + + - + + + + + + + + + - app:layout_constraintStart_toStartOf="@id/tv_business_info_title" - app:layout_constraintTop_toBottomOf="@id/tv_business_info_title"/> + - app:layout_constraintStart_toStartOf="@+id/tv_business_info" - app:layout_constraintTop_toBottomOf="@id/tv_business_info"/> + + + + - - - - + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_magazine_list.xml b/presentation/src/main/res/layout/fragment_magazine_list.xml index 64d11110..57632f48 100644 --- a/presentation/src/main/res/layout/fragment_magazine_list.xml +++ b/presentation/src/main/res/layout/fragment_magazine_list.xml @@ -118,10 +118,10 @@ android:layout_height="match_parent" android:orientation="vertical"> - - - - + + + + diff --git a/presentation/src/main/res/layout/fragment_medicine_check.xml b/presentation/src/main/res/layout/fragment_medicine_check.xml index a9f30690..f337bb1b 100644 --- a/presentation/src/main/res/layout/fragment_medicine_check.xml +++ b/presentation/src/main/res/layout/fragment_medicine_check.xml @@ -2,7 +2,7 @@ + tools:context=".selfassessment.fragment.SelfAssessmentFragment"> + android:paddingBottom="50dp" + android:background="@color/white"> - - - - - - - - - + app:layout_constraintTop_toBottomOf="@+id/tv_user_birth_day_view" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + /> + app:layout_constraintTop_toBottomOf="@+id/ivBannerMedicine"> + app:layout_constraintTop_toBottomOf="@+id/ivBannerMedicine"> + app:layout_constraintTop_toBottomOf="@+id/ivBannerMedicine"> + app:layout_constraintRight_toRightOf="@id/ivBannerMedicine" + app:layout_constraintTop_toBottomOf="@+id/ivBannerMedicine"> - + - - - + app:layout_constraintStart_toStartOf="parent"/> + + - + + app:layout_constraintTop_toBottomOf="@id/clCategoryFunction"> + + + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/menu_device_setting"> + + + + + + + + + + + android:paddingVertical="18dp" + app:layout_constraintTop_toBottomOf="@id/clCategoryCustomerService"> - - diff --git a/presentation/src/main/res/layout/fragment_reserv_detail.xml b/presentation/src/main/res/layout/fragment_reserv_detail.xml index 280e1f79..a7474fbf 100644 --- a/presentation/src/main/res/layout/fragment_reserv_detail.xml +++ b/presentation/src/main/res/layout/fragment_reserv_detail.xml @@ -220,7 +220,7 @@ android:fontFamily="@font/pretendard_medium" android:includeFontPadding="false" - android:text="07-28" + android:text="09-23" android:textColor="@color/gray1_42" android:textSize="@dimen/M500_16_h22" @@ -320,7 +320,7 @@ android:layout_height="wrap_content" android:layout_marginTop="20dp" - android:fontFamily="@font/pretendard_regular" + android:fontFamily="@font/pretendard_medium" android:includeFontPadding="false" android:text="예약 2일전 부터는 취소 및\n변경시 수수료가 발생합니다." android:textColor="@color/gray1_42" @@ -347,7 +347,7 @@ android:id="@+id/cl_empty_layout" android:layout_width="0dp" android:layout_height="0dp" - + android:visibility="invisible" app:layout_constraintBottom_toBottomOf="@id/iv_ticket_bottom" app:layout_constraintEnd_toEndOf="@id/iv_ticket_top" app:layout_constraintStart_toStartOf="@id/iv_ticket_top" diff --git a/presentation/src/main/res/layout/fragment_self_assessment.xml b/presentation/src/main/res/layout/fragment_self_assessment.xml index 38e18db8..5b34d4e2 100644 --- a/presentation/src/main/res/layout/fragment_self_assessment.xml +++ b/presentation/src/main/res/layout/fragment_self_assessment.xml @@ -2,9 +2,10 @@ + tools:context=".selfassessment.fragment.SelfAssessmentFragment"> @@ -31,7 +32,7 @@ app:layout_constraintBottom_toBottomOf="parent"> diff --git a/presentation/src/main/res/layout/fragment_stats.xml b/presentation/src/main/res/layout/fragment_stats.xml index bb51d30c..651dc786 100644 --- a/presentation/src/main/res/layout/fragment_stats.xml +++ b/presentation/src/main/res/layout/fragment_stats.xml @@ -13,69 +13,80 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> - - - - + app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintEnd_toEndOf="parent"/> - - app:cv_daySize="rectangle" - app:cv_dayViewResource="@layout/layout_calendar_day_cell" + + app:layout_constraintTop_toBottomOf="@id/layout_common_app_bar"/> - - app:layout_constraintBottom_toBottomOf="@id/cl_week_calendar" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" /> + + + - + app:layout_constraintTop_toBottomOf="@id/clHandler"> - + \ No newline at end of file diff --git a/presentation/src/main/res/layout/layout_assessment_faq.xml b/presentation/src/main/res/layout/layout_assessment_faq.xml new file mode 100644 index 00000000..d1548f5d --- /dev/null +++ b/presentation/src/main/res/layout/layout_assessment_faq.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/layout_assessment_header.xml b/presentation/src/main/res/layout/layout_assessment_header.xml new file mode 100644 index 00000000..bedda502 --- /dev/null +++ b/presentation/src/main/res/layout/layout_assessment_header.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/layout_assessment_recommend_video.xml b/presentation/src/main/res/layout/layout_assessment_recommend_video.xml new file mode 100644 index 00000000..3333ce66 --- /dev/null +++ b/presentation/src/main/res/layout/layout_assessment_recommend_video.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/layout_assessment_stats.xml b/presentation/src/main/res/layout/layout_assessment_stats.xml new file mode 100644 index 00000000..44c8aa92 --- /dev/null +++ b/presentation/src/main/res/layout/layout_assessment_stats.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/layout_calendar_day_of_week_cell.xml b/presentation/src/main/res/layout/layout_calendar_day_of_week_cell.xml new file mode 100644 index 00000000..8ae219c0 --- /dev/null +++ b/presentation/src/main/res/layout/layout_calendar_day_of_week_cell.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/layout_common_app_bar.xml b/presentation/src/main/res/layout/layout_common_app_bar.xml index afef9775..5e375c71 100644 --- a/presentation/src/main/res/layout/layout_common_app_bar.xml +++ b/presentation/src/main/res/layout/layout_common_app_bar.xml @@ -22,6 +22,30 @@ app:layout_constraintBottom_toBottomOf="parent" app:tint="@color/gray1_42" /> + + + + + + + + + + + diff --git a/presentation/src/main/res/layout/sub_banner_item.xml b/presentation/src/main/res/layout/sub_banner_item.xml new file mode 100644 index 00000000..94df03ea --- /dev/null +++ b/presentation/src/main/res/layout/sub_banner_item.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/sub_faq_item.xml b/presentation/src/main/res/layout/sub_faq_item.xml new file mode 100644 index 00000000..71412380 --- /dev/null +++ b/presentation/src/main/res/layout/sub_faq_item.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/sub_magazine_item.xml b/presentation/src/main/res/layout/sub_full_size_magazine_item.xml similarity index 100% rename from presentation/src/main/res/layout/sub_magazine_item.xml rename to presentation/src/main/res/layout/sub_full_size_magazine_item.xml diff --git a/presentation/src/main/res/menu/navigation_menu.xml b/presentation/src/main/res/menu/navigation_menu.xml index 5c30b1e7..49c54a69 100644 --- a/presentation/src/main/res/menu/navigation_menu.xml +++ b/presentation/src/main/res/menu/navigation_menu.xml @@ -15,23 +15,23 @@ /> \ No newline at end of file diff --git a/presentation/src/main/res/raw/lottie_scan.json b/presentation/src/main/res/raw/lottie_scan.json new file mode 100644 index 00000000..36a3b99d --- /dev/null +++ b/presentation/src/main/res/raw/lottie_scan.json @@ -0,0 +1 @@ +{"nm":"scan","ddd":0,"h":700,"w":700,"meta":{"g":"@lottiefiles/toolkit-js 0.26.1"},"layers":[{"ty":3,"nm":"Null 2","sr":1,"st":0,"op":155.000006313279,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.534,"y":0},"i":{"x":0.109,"y":1},"s":[353.303,143,0],"t":10,"ti":[0,-0.724,0],"to":[1.769,70.214,0]},{"h":1,"s":[353.303,518,0],"t":36.973},{"o":{"x":0.823,"y":0},"i":{"x":0.453,"y":1},"s":[353.303,518,0],"t":43,"ti":[-0.082,-3.252,0],"to":[0,176.667,0]},{"s":[353.303,143,0],"t":72.0000029326201}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":0,"ix":11}},"ef":[],"ind":1},{"ty":4,"nm":"Shape Layer 13","sr":1,"st":0,"op":155.000006313279,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[2.955,26.75,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[-3.303,-0.25,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":3,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"s":{"a":0,"k":[360.455,7.5],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.0235,0.5686,0.9569],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[2.955,26.75],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2,"parent":1},{"ty":4,"nm":"Shape Layer 14","sr":1,"st":0,"op":155.000006313279,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[2.955,26.75,0],"ix":1},"s":{"a":0,"k":[100,220,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[-3.303,-0.25,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[{"ty":0,"mn":"ADBE Gaussian Blur 2","nm":"Gaussian Blur","ix":1,"en":1,"ef":[{"ty":0,"mn":"ADBE Gaussian Blur 2-0001","nm":"Blurriness","ix":1,"v":{"a":0,"k":8,"ix":1}},{"ty":7,"mn":"ADBE Gaussian Blur 2-0002","nm":"Blur Dimensions","ix":2,"v":{"a":0,"k":1,"ix":2}},{"ty":7,"mn":"ADBE Gaussian Blur 2-0003","nm":"Repeat Edge Pixels","ix":3,"v":{"a":0,"k":0,"ix":3}},{"ty":7,"mn":"ADBE Force CPU GPU","nm":"GPU Rendering","ix":4,"v":{"a":0,"k":1,"ix":4}}]}],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":3,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"s":{"a":0,"k":[360.455,7.5],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.0235,0.5686,0.9569],"ix":4},"r":1,"o":{"a":0,"k":60,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[2.955,26.75],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3,"parent":1},{"ty":4,"nm":"Shape Layer 10","sr":1,"st":0,"op":161.000006557664,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-38.182,2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[521.1,180.5,0],"ix":2},"r":{"a":0,"k":90,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-39.091,2],[22.727,2]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"c":{"a":0,"k":[0.1804,0.2745,0.3608],"ix":3}},{"ty":"tr","a":{"a":0,"k":[-8.182,1.5],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-8.182,1.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":4},{"ty":4,"nm":"Shape Layer 9","sr":1,"st":0,"op":161.000006557664,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-38.182,2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[521.1,180.5,0],"ix":2},"r":{"a":0,"k":180,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-39.091,2],[22.727,2]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"c":{"a":0,"k":[0.1804,0.2745,0.3608],"ix":3}},{"ty":"tr","a":{"a":0,"k":[-8.182,1.5],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-8.182,1.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":5},{"ty":4,"nm":"Shape Layer 8","sr":1,"st":0,"op":161.000006557664,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-38.182,2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[521.1,484.5,0],"ix":2},"r":{"a":0,"k":180,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-39.091,2],[22.727,2]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"c":{"a":0,"k":[0.1804,0.2745,0.3608],"ix":3}},{"ty":"tr","a":{"a":0,"k":[-8.182,1.5],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-8.182,1.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":6},{"ty":4,"nm":"Shape Layer 7","sr":1,"st":0,"op":161.000006557664,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-38.182,2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[521.1,484.5,0],"ix":2},"r":{"a":0,"k":270,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-39.091,2],[22.727,2]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"c":{"a":0,"k":[0.1804,0.2745,0.3608],"ix":3}},{"ty":"tr","a":{"a":0,"k":[-8.182,1.5],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-8.182,1.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":7},{"ty":4,"nm":"Shape Layer 6","sr":1,"st":0,"op":161.000006557664,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-38.182,2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[184.1,180.5,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-39.091,2],[22.727,2]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"c":{"a":0,"k":[0.1804,0.2745,0.3608],"ix":3}},{"ty":"tr","a":{"a":0,"k":[-8.182,1.5],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-8.182,1.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":8},{"ty":4,"nm":"Shape Layer 5","sr":1,"st":0,"op":161.000006557664,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-38.182,2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[184.1,180.5,0],"ix":2},"r":{"a":0,"k":90,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-39.091,2],[22.727,2]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"c":{"a":0,"k":[0.1804,0.2745,0.3608],"ix":3}},{"ty":"tr","a":{"a":0,"k":[-8.182,1.5],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-8.182,1.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":9},{"ty":4,"nm":"Shape Layer 4","sr":1,"st":0,"op":161.000006557664,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-38.182,2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[184.1,484.5,0],"ix":2},"r":{"a":0,"k":-90,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-39.091,2],[22.727,2]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"c":{"a":0,"k":[0.1804,0.2745,0.3608],"ix":3}},{"ty":"tr","a":{"a":0,"k":[-8.182,1.5],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-8.182,1.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":10},{"ty":4,"nm":"Shape Layer 3","sr":1,"st":0,"op":161.000006557664,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-38.182,2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[184.1,484.5,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-39.091,2],[22.727,2]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"c":{"a":0,"k":[0.1804,0.2745,0.3608],"ix":3}},{"ty":"tr","a":{"a":0,"k":[-8.182,1.5],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-8.182,1.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":11},{"ty":5,"nm":"Scanning...","sr":1,"st":0,"op":254.000010345632,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0.402,-8.235,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,599.947,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"t":{"m":{"a":{"a":0,"k":[0,0],"ix":2},"g":1},"p":{},"a":[],"d":{"k":[{"s":{"f":"Vision-Regular","s":34,"t":"Scanning...","fc":[0.0314,0.3412,0.6275],"lh":40.8,"tr":0,"j":2,"ca":0},"t":0}]}},"ind":12}],"v":"5.6.7","fr":29.9700012207031,"op":101.000004113814,"ip":0,"fonts":{"list":[{"ascent":69.9996948242188,"fClass":"","fFamily":"Vision","fStyle":"Regular","fName":"Vision-Regular","fPath":"","fWeight":"","origin":0}]},"chars":[{"ch":"S","fFamily":"Vision","size":34,"style":"Regular","w":54.1,"data":{"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"S","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"S","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[5.438,0],[0,-11.884],[0,-13.394],[6.244,0],[3.625,2.719],[0,0],[-6.546,0],[0,12.085],[0,11.078],[-5.841,0],[-3.122,-2.518],[0,0]],"o":[[-10.474,0],[0,18.43],[0,6.949],[-4.431,0],[0,0],[5.237,4.028],[11.783,0],[0,-18.832],[0,-6.949],[4.028,0],[0,0],[-4.129,-3.726]],"v":[[30.212,-62.64],[9.366,-45.721],[38.773,-17.221],[26.184,-7.15],[10.474,-12.891],[6.747,-5.237],[26.889,1.007],[47.736,-18.631],[18.127,-45.419],[31.119,-54.886],[43.607,-50.757],[46.527,-57.404]]},"ix":2}}]}]}},{"ch":"c","fFamily":"Vision","size":34,"style":"Regular","w":47.7,"data":{"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"c","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"c","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[5.942,0],[0,-14.401],[-15.005,0],[-4.23,4.431],[0,0],[4.633,0],[0,9.366],[-10.172,0],[-3.122,-3.223],[0,0]],"o":[[-15.106,0],[0,13.998],[6.042,0],[0,0],[-3.223,3.223],[-9.97,0],[0,-9.467],[4.431,0],[0,0],[-4.028,-4.431]],"v":[[29.81,-52.771],[5.438,-25.882],[29.507,1.007],[47.031,-6.143],[43.204,-12.689],[30.515,-7.15],[13.797,-25.983],[30.917,-44.916],[43.204,-39.377],[47.031,-45.923]]},"ix":2}}]}]}},{"ch":"a","fFamily":"Vision","size":34,"style":"Regular","w":51.8,"data":{"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"a","ix":1,"cix":2,"np":5,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"a","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[30.917,0],[4.23,-4.23],[0,0],[-4.935,0],[-0.201,-7.251],[5.64,0],[0,-9.567],[-10.675,0],[-4.028,4.028],[-4.23,-0.101],[0,0]],"o":[[-6.042,0],[0,0],[3.726,-3.122],[9.668,0],[-3.223,-2.618],[-9.366,0],[0,10.675],[5.64,0],[1.813,3.726],[0,0],[-14.2,0.201]],"v":[[23.566,-52.872],[6.747,-46.024],[9.567,-39.981],[22.961,-45.419],[35.147,-30.212],[21.652,-34.644],[4.028,-16.718],[21.652,1.007],[36.96,-5.237],[46.527,1.007],[49.548,-5.539]]},"ix":2}},{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"a","ix":2,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-5.64,0],[-2.921,-3.021],[0,0],[4.532,0],[0,6.345]],"o":[[5.539,0],[0,0],[-3.021,3.525],[-6.445,0],[0,-5.74]],"v":[[23.062,-27.09],[35.248,-22.659],[35.55,-11.682],[23.062,-6.244],[12.085,-16.718]]},"ix":2}}]}]}},{"ch":"n","fFamily":"Vision","size":34,"style":"Regular","w":54.6,"data":{"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"n","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"n","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[12.79,0],[2.014,-5.64],[0,0],[0,0],[0,0],[0,0],[0,0],[-10.272,0],[0,-8.057],[0,0],[0,0],[0,0]],"o":[[-5.942,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,-7.452],[9.467,0],[0,0],[0,0],[0,0],[0,-11.481]],"v":[[29.709,-52.872],[15.71,-43.103],[15.71,-51.764],[7.654,-51.764],[7.654,0],[15.71,0],[15.71,-25.882],[27.997,-44.211],[38.974,-26.99],[38.974,0],[46.93,0],[46.93,-32.025]]},"ix":2}}]}]}},{"ch":"i","fFamily":"Vision","size":34,"style":"Regular","w":25.5,"data":{"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"i","ix":1,"cix":2,"np":5,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"i","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,2.719],[2.82,0],[0,-2.82],[-2.719,0]],"o":[[0,-2.82],[-2.719,0],[0,2.82],[2.82,0]],"v":[[18.027,-62.54],[12.79,-67.575],[7.654,-62.54],[12.79,-57.504]]},"ix":2}},{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"i","ix":2,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16.818,-51.764],[8.762,-51.764],[8.762,0],[16.818,0]]},"ix":2}}]}]}},{"ch":"g","fFamily":"Vision","size":34,"style":"Regular","w":55.8,"data":{"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"g","ix":1,"cix":2,"np":5,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"g","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[4.834,0],[0,-13.797],[-14.502,0],[-3.625,3.021],[9.366,0],[2.719,2.618],[0,0],[-5.338,0],[-0.705,13.092],[0,2.014],[0,0],[0,0],[0,0]],"o":[[-14.401,0],[0,13.495],[4.633,0],[0,8.56],[-3.827,0],[0,0],[4.028,3.827],[14.703,0],[0.101,-2.014],[0,0],[0,0],[0,0],[-3.323,-3.525]],"v":[[28.5,-52.872],[5.438,-27.896],[28.299,-2.115],[41.492,-6.244],[26.587,11.279],[12.891,6.647],[9.265,13.193],[27.09,19.135],[49.347,-6.747],[49.448,-12.79],[49.448,-51.764],[41.492,-51.764],[41.492,-47.635]]},"ix":2}},{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"g","ix":2,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-9.869,0],[-3.122,-3.726],[0,0],[4.33,0],[0,9.164]],"o":[[4.935,0],[0,0],[-2.014,4.33],[-9.869,0],[0,-9.366]],"v":[[29.407,-45.218],[41.592,-39.478],[41.592,-15.912],[29.205,-10.071],[13.898,-27.997]]},"ix":2}}]}]}},{"ch":".","fFamily":"Vision","size":34,"style":"Regular","w":21.3,"data":{"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":".","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":".","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,3.424],[3.424,0],[0,-3.424],[-3.424,0]],"o":[[0,-3.424],[-3.424,0],[0,3.424],[3.424,0]],"v":[[16.919,-5.237],[10.776,-11.481],[4.532,-5.237],[10.776,1.007]]},"ix":2}}]}]}}],"assets":[]} \ No newline at end of file diff --git a/presentation/src/main/res/values/array.xml b/presentation/src/main/res/values/array.xml new file mode 100644 index 00000000..17842c8e --- /dev/null +++ b/presentation/src/main/res/values/array.xml @@ -0,0 +1,191 @@ + + + + + 안면비대칭 예방 방법은 무엇인가요 ? + 안면비대칭이 건강에도 영향을 미치나요 ? + 안면비대칭의 종류와 원인이 궁금합니다. + 안면비대칭도 자가 교정법이 있나요 ? + + + + 안면비대칭의 원인은 선천적 / 후천적인 원인으로 나타나는데 대부분이 후천적인 원인인 잘못된 생활습관, + 좋지 않은 자세, 외부의 압박 등으로 나타나게 됩니다. \n\n + 이러한 원인들은 최종적으로 내부속의 악관절과 경추의 구조를 무너뜨리면서 안면비대칭이 나타나는 것입니다. 때문에 평소 자신의 생활 습관을 되돌아보고 고치려는 노력이 필요합니다. + + \n\n1. 딱딱한 음식을 한쪽으로 많이 씹지 말아야 합니다. + + \n\n2. 한쪽으로 치우쳐 앉거나 다리를 꼬지 말아야 합니다. + + \n\n3. 턱 괴는 습관을 고쳐야 합니다. + + \n\n4. 걷거나 앉을 때 자세를 바르고 곧게 유지해야 합니다. + + \n\n5. 규칙적인 운동이나 가벼운 전신스트레칭을 해야 합니다. + + \n\n6. 잠을 잘 때 바른 자세로 누워서 잠을 청하도록 합니다. + + \n\n이처럼 사소한 습관에도 안면비대칭을 유발하고, 악화시킬 수 있으므로 ToothFairy의 자가 진단 시스템을 통해 주기적으로 진단할 것을 권장합니다. + + + 안면비대칭은 얼굴뿐만 아니라 전체적으로 영향을 미칠 수 있습니다. 대표적인 증상은 아래와 같습니다. + \n\n 1. 턱관절 질환 (턱관절의 통증, 소리, 개구장애 등) + \n\n 2. 통증(안구통, 두통, 목의 통증) + \n\n 3. 체형 불균형(거북목이나 일자목, 척추측만, 골반경사, 다리길이 불균형 등) + \n\n 4. 두개 혈액, 림프순환 장애 + \n\n 5. 신경전달 장애로 인한 소화불량, 생리불순, 변비/설사, 집중력저하 등 + \n\n 위와 같이 안면비대칭은 전반적인 부분에 영향을 끼친다고 볼 수 있습니다. ToothFairy의 자가 진단 시스템을 통해 주기적인 검사로 미리 예방하는 것이 중요합니다. + + + + 안면비대칭은 크게 선천적으로 발생한 `골격성 안면비대칭`과 다양한 후천적 원인으로 나타나는 `비골격성 안면비대칭`으로 나뉩니다. + \n\n 1. 골격성 안면비대칭 + \n\t - 선천적, 유전적인 원인으로 발생 + \n\t - 안면골격이 비정상적으로 성장하거나 덜 성장해 발생 + \n\n 2. 비골격성 안면비대칭 + \n\t - 여러가지 후천적 요인들이 복합적으로 작용하여 발생 + \n\t - 얼굴 양쪽 근육의 불균형을 초래 + \n\t - 하악형, 측두골형, 접형골형 등으로 나뉨 + + + + 몸이 틀어지면 체형 교정을 하고, 치아가 틀어지면 치아 교정을 합니다. 안면비대칭도 교정을 하면 좌우 균형을 맞추고 윤곽을 가지런히 정돈 하는데 도움이 됩니다. + \n\n안면비대칭 자가 교정법의 핵심은 스스로 마사지‧지압‧스트레칭‧근육 강화 운동을 해서 불균형한 근육 기능을 정상적으로 회복 시키는 것 입니다. + \n\n자가 교정 운동을 통해 짧아진 얼굴 근육을 정상적인 길이로 늘려주고, 약해진 근육은 강화 시켜 비대칭을 개선하는 할 수 있습니다. + + \n\n자세한 자가 교정 방법은 아래의 추천 영상을 확인해주세요. + + + + + + 턱에서 소리가나는데 더 심해질까요 ? + 어떤 치료를 가장 먼저 받아야하나요 ? + 뼈끼리 부딫혀서 턱이 갈려도 교정 되나요 ? + + + + + 턱에서 소리가 나는 이유는 한쪽으로 턱이 틀어지면서 턱관절이 놓인 위치가 불균형 해졌다는 뜻입니다. + 균형이 깨진 상태를 방치하게 되면 비대칭이 심해질 가능성이 높기 때문에, 교정치료가 필요합니다. + + + + 전신불균형을 교정하며 그 위에 두개안면골이 안정적으로 놓이도록 위치를 수정해 가는 과정이 안면비대칭 교정치료입니다. + 이 과정에서 얼굴 안면비대칭 뿐만 아니라 거북목, 척추측만증 등의 기저증상까지 모두 개선이 됩니다. + \n\n어떤 치료를 받을지 고민하는 것보다 하루라도 빨리 의사와의 상담을 통해 미리 대처하는게 좋습니다. + + + + 마모된 뼈를 재생하는 것은 쉽지 않으나, 턱의 좌우균형을 맞춰주는 편차치료를 통해 턱에서 나는 소리나 통증 등의 증상을 없애고 + 추가적인 구조적 변형을 예방할 수 있습니다. + + + + + + 측두골형이 나왔어요. 어떻게 해야하나요 ? + 마사지나 습관만으로 교정이 가능한가요 ? + 안면비대칭을 방치하면 어떻게 되나요 ? + 안면비대칭도 자가 교정법이 있나요 ? + + + + + 선천적인 미세한 차이라면 수술이 도움 될 수 있습니다.\n\n + 하지만 외상이나 평소 잘못된 자세 습관 누적 등으로 나타난 후천적인 얼굴비대칭은 핵심 골격 문제를 정밀하게 개선해주는 비수술요법으로도 충분히 교정이 가능합니다.\n\n + 특히 시술, 수술 등을 통해 인위적으로 겉형태만 개선한다면 금세 재발하거나 원인모를 통증으로 이어질 수 있어 근본적인 교정에 대한 적극적인 주의를 요합니다. + + + + 한쪽으로만 음식을 씹거나 턱 괴기, 엎드려 자기 등은 비대칭얼굴을 만드는 주요 원인입니다.\n\n + 이때 반대쪽으로만 움직이거나 자극을 주고 마시지나 자가 교정기 등을 따르는 경우가 드물지 않습니다.\n\n + 물론 마사지는 경직된 근육을 풀어주는데 도움 줄 수 있습니다.\n\n + 그러나 안면비대칭은 턱 관절과 경추가 어긋나면서 두개골 변위로 나타나는 원리로 습관 개선만으로 근원 문제가 해결되기는 어렵습니다.\n\n + 교정기 또한 개인마다 다른 증상을 고려하지 못하여 자칫 악화를 부를 수 있으니, 전문 의사 선생님과 상담을 받아볼 것을 요합니다. + + + + 안면비대칭 환자의 경우 선천적인 안면 비대칭 때문에 고민 하다 병원을 찾는 경우도 있지만,\n\n + 평소에 잘 모르고 있다가 사진을 찍었을 때 턱이 삐뚤어져 보이거나 주변으로부터 얼굴이 틀어졌다는 이야기를 듣고 뒤늦게 병원을 찾는 경우도 있습니다.\n\n + 안면비대칭이 심해지면 씹는데 불편을 느끼거나 치아에 편마모가 생기는 등 기능적인 측면에서 문제가 생길 수 있으므로 절대 방치해선 안됩니다. + + + + 몸이 틀어지면 체형 교정을 하고, 치아가 틀어지면 치아 교정을 합니다.\n\n 안면비대칭도 교정을 하면 좌우 균형을 맞추고 윤곽을 가지런히 정돈 하는데 도움이 됩니다.\n\n + 안면비대칭 자가 교정법의 핵심은 스스로 마사지‧지압‧스트레칭‧근육 강화 운동을 해서 불균형한 근육 기능을 정상적으로 회복 시키는 것 입니다.\n\n + 자가 교정 운동을 통해 짧아진 얼굴 근육을 정상적인 길이로 늘려주고, 약해진 근육은 강화 시켜 비대칭을 개선하는 것입니다. + + \n\n자세한 자가 교정 방법은 아래의 추천 영상을 확인해주세요. + + + + + + 수술을 무조건 해야하나요 ? + 마사지나 습관만으로 교정이 가능한가요 ? + 안면비대칭을 방치하면 어떻게 되나요 ? + 안면비대칭도 자가 교정법이 있나요 ? + + + + + 선천적인 미세한 차이라면 수술이 도움 될 수 있습니다.\n\n + 하지만 외상이나 평소 잘못된 자세 습관 누적 등으로 나타난 후천적인 얼굴비대칭은 핵심 골격 문제를 정밀하게 개선해주는 비수술요법으로도 충분히 교정이 가능합니다.\n\n + 특히 시술, 수술 등을 통해 인위적으로 겉형태만 개선한다면 금세 재발하거나 원인모를 통증으로 이어질 수 있어 근본적인 교정에 대한 적극적인 주의를 요합니다. + + + + 한쪽으로만 음식을 씹거나 턱 괴기, 엎드려 자기 등은 비대칭얼굴을 만드는 주요 원인입니다.\n\n + 이때 반대쪽으로만 움직이거나 자극을 주고 마시지나 자가 교정기 등을 따르는 경우가 드물지 않습니다.\n\n + 물론 마사지는 경직된 근육을 풀어주는데 도움 줄 수 있습니다.\n\n + 그러나 안면비대칭은 턱 관절과 경추가 어긋나면서 두개골 변위로 나타나는 원리로 습관 개선만으로 근원 문제가 해결되기는 어렵습니다.\n\n + 교정기 또한 개인마다 다른 증상을 고려하지 못하여 자칫 악화를 부를 수 있으니, 전문 의사 선생님과 상담을 받아볼 것을 요합니다. + + + + 안면비대칭 환자의 경우 선천적인 안면 비대칭 때문에 고민 하다 병원을 찾는 경우도 있지만,\n\n + 평소에 잘 모르고 있다가 사진을 찍었을 때 턱이 삐뚤어져 보이거나 주변으로부터 얼굴이 틀어졌다는 이야기를 듣고 뒤늦게 병원을 찾는 경우도 있습니다.\n\n + 안면비대칭이 심해지면 씹는데 불편을 느끼거나 치아에 편마모가 생기는 등 기능적인 측면에서 문제가 생길 수 있으므로 절대 방치해선 안됩니다. + + + + 몸이 틀어지면 체형 교정을 하고, 치아가 틀어지면 치아 교정을 합니다.\n\n 안면비대칭도 교정을 하면 좌우 균형을 맞추고 윤곽을 가지런히 정돈 하는데 도움이 됩니다.\n\n + 안면비대칭 자가 교정법의 핵심은 스스로 마사지‧지압‧스트레칭‧근육 강화 운동을 해서 불균형한 근육 기능을 정상적으로 회복 시키는 것 입니다.\n\n + 자가 교정 운동을 통해 짧아진 얼굴 근육을 정상적인 길이로 늘려주고, 약해진 근육은 강화 시켜 비대칭을 개선하는 것입니다. + + \n\n자세한 자가 교정 방법은 아래의 추천 영상을 확인해주세요. + + + + + + + 칫솔모가 교정이랑 연관이 있나요 ? + 교정용 칫솔이 따로 있나요 ? + 올바른 칫솔질 방법이 궁금해요 + + + + + 칫솔모가 벌어지게 되면 칫솔은 고유의 기능을 상실하게 됩니다.\n\n + 교정기를 부착하고나면, 편평했던 치면에 여러 구석진 곳들(교정기의 상/하; 치면과 철사 사이)이 생기면서 음식물이 끼는 곳이 더 많아지게 됩니다.\n\n + 이를 철저히 닦아내지 않으면 여러 개의 충치와 잇몸 질환에 이환 되거나. 교정 치료 기간이 심하게 연장 되거나, 교정 치료 결과가 불량 해질 수 있습니다. + + + + 교정용 칫솔이 따로 존재합니다.\n\n + 교정용 칫솔은 일반 칫솔에 비해 칫솔모의 크기가 작고 홈이 파여있는 것도 있으며 최근에는 칫솔모의 모양과 굵기가 다른 교정용 칫솔도 시중에 나와 있습니다.\n\n + 이에 대한 연구는 그리 많이 진행 되지는 않았으나 몇 가지 연구들의 결과를 종합해보면 일반적인 칫솔에 비해 교정용 칫솔이 교정 환자의 구강 내 치태 제거 효율이 월등히 뛰어납니다.\n\n + 그 효과에 대해서는 여러 가지 의견이 있지만 일반적인 칫솔모 보다 미세모를 가진 칫솔이 교정 환자들에게는 더 적합하다는 보고도 있습니다. + + + + 1. 칫솔을 잡을 때에는 과도하게 힘을 주지 말고, 가볍게 볼펜을 잡듯 잡습니다.\n\n + 2. 칫솔질은 너무 빠르지 않게, 천천히 부드럽게 시간을 들여 치아를 구석구석 닦아줍니다.\n\n + 3. 치실로 치아 사이의 잔여물을 확실하게 제거하는 것도 중요합니다.\n\n + 4. 치간 칫솔, 워터픽을 사용하는 것도 효과적입니다. + + + + \ No newline at end of file diff --git a/presentation/src/main/res/values/colors.xml b/presentation/src/main/res/values/colors.xml index 0f311178..f4013d1e 100644 --- a/presentation/src/main/res/values/colors.xml +++ b/presentation/src/main/res/values/colors.xml @@ -16,9 +16,10 @@ #B3000000 #80000000 #4D000000 - #202020 + #0073FA + #1B9AFF #7CBDFF #4780EE #E7F3FF diff --git a/presentation/src/main/res/xml/fragment_stats_scene.xml b/presentation/src/main/res/xml/fragment_stats_scene.xml new file mode 100644 index 00000000..8fcc900e --- /dev/null +++ b/presentation/src/main/res/xml/fragment_stats_scene.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + +