Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2e70646
feat: 리소스 추가 & 정리 (#193)
dongx0915 Sep 17, 2023
a797c9f
chore: Mock 서버 Url 변경(#193)
dongx0915 Sep 18, 2023
7c38893
chore: 기타 수정 사항 반영(#193)
dongx0915 Sep 18, 2023
544a4f6
feat: 홈 화면 디자인 개편(#193)
dongx0915 Sep 18, 2023
0b9c922
feat: 매거진 디자인 변경 & 타임라인 제거(#193)
dongx0915 Sep 18, 2023
0abeebc
feat: 프로필 디자인 개편(#193)
dongx0915 Sep 18, 2023
d5aa8d2
feat: 홈 화면 유튜브 영상 클릭시 유튜브 앱으로 랜딩(#193)
dongx0915 Sep 18, 2023
f244519
feat: 메뉴 탭에 병원 지도 화면 추가(#193)
dongx0915 Sep 18, 2023
c55419c
fix: BlurView 깨지는 현상 수정(#193)
dongx0915 Sep 18, 2023
d6c9b53
feat: 기타 추가 사항 업로드 (#193)
dongx0915 Sep 18, 2023
6101d24
feat: 홈화면 요소 추가 및 수정 (#193)
android-donghyeon Oct 11, 2023
fb5b035
feat: 리소스 추가 및 수정 (#193)
dongx0915 Nov 14, 2023
093c0c8
feat: 확장 함수 추가 및 수정 (#193)
dongx0915 Nov 14, 2023
406d30b
feat: 임시 서버 주소 수정
dongx0915 Nov 14, 2023
b2bba04
feat: 칫솔모 교체 주기 검사 기능 구현
dongx0915 Nov 14, 2023
932d730
feat: 안면 인식 기능 구현 (#193)
dongx0915 Nov 14, 2023
21c6d74
feat: 데코레이터 추가 (#193)
dongx0915 Nov 14, 2023
86a0975
feat: 측정 결과 화면 구현 (#193)
dongx0915 Nov 14, 2023
ab1b3af
feat: 기타 화면 수정 사항 반영 (#193)
dongx0915 Nov 14, 2023
6543b71
fix: 교정기 착용 알림 테스트 코드 제거 (#193)
dongx0915 Nov 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions data/src/main/java/com/example/data/hilt/module/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down
34 changes: 34 additions & 0 deletions data/src/main/java/com/example/data/toothbrush/ToothBrushModule.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<ToothBrushResponse>
}
Original file line number Diff line number Diff line change
@@ -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<ToothBrushResponse, ToothBrush> {
override fun ToothBrushResponse.toDomainModel(): ToothBrush {
return ToothBrush(
Base64.decode(image, Base64.DEFAULT)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ResponseState<ToothBrush>> {
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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Faq>) : Assessment() {

constructor(question: Array<String>, answer: Array<String>): 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<RecommendVideo>
): Assessment() {

/**
* Thumbnail(ByteArray, Resource)
* 두 가지 중 하나의 타입으로 전달 받을 수 있음
*/
@Parcelize
data class RecommendVideo(
val title: String,
val youtubeUrl: String,
@DrawableRes val thumbnailRes: Int? = null,
val thumbnail: ByteArray? = null,
) : BaseModel
}
}


18 changes: 18 additions & 0 deletions domain/src/main/java/com/example/domain/banner/Banner.kt
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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<ResponseState<ToothBrush>>
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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<ResponseState<ToothBrush>>{
return toothBrushRepository.checkToothBrushStatus(userId, image)
}
}
11 changes: 11 additions & 0 deletions presentation/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
}
13 changes: 13 additions & 0 deletions presentation/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@
<!-- BLE 지원 기기만 제공 -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

<!-- Camera X 권한 -->
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

<!-- 저장소 접근 권한 (android 13 이상)-->
<!-- 사진만 저장할 것이기 때문에 images만 요청 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<application
android:name=".App"
android:allowBackup="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.example.smiley.common.customview

import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.Transformation

class ToggleAnimation {

companion object {

fun toggleArrow(view: View, isExpanded: Boolean): Boolean {
return if (isExpanded) {
view.animate().setDuration(200).rotation(180f)
true
} else {
view.animate().setDuration(200).rotation(0f)
false
}
}

fun expand(view: View) {
val animation = expandAction(view)
view.startAnimation(animation)
}

private fun expandAction(view: View) : Animation {
view.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val actualHeight = view.measuredHeight

view.layoutParams.height = 0
view.visibility = View.VISIBLE

val animation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
view.layoutParams.height = if (interpolatedTime == 1f) ViewGroup.LayoutParams.WRAP_CONTENT
else (actualHeight * interpolatedTime).toInt()

view.requestLayout()
}
}

animation.duration = (actualHeight / view.context.resources.displayMetrics.density).toLong()

view.startAnimation(animation)

return animation
}

fun collapse(view: View) {
val actualHeight = view.measuredHeight

val animation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
if (interpolatedTime == 1f) {
view.visibility = View.GONE
} else {
view.layoutParams.height = (actualHeight - (actualHeight * interpolatedTime)).toInt()
view.requestLayout()
}
}
}

animation.duration = (actualHeight / view.context.resources.displayMetrics.density).toLong()
view.startAnimation(animation)
}
}

}
Loading