From 6b31059e275d231d71ba68c78daf04eb047ce008 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 01:32:54 +0900 Subject: [PATCH 01/23] feat: implement CertifyMissionRequest --- .../data/model/home/CertifyMissionRequest.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionRequest.kt diff --git a/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionRequest.kt b/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionRequest.kt new file mode 100644 index 0000000..6579f2c --- /dev/null +++ b/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionRequest.kt @@ -0,0 +1,42 @@ +package univ.earthbreaker.namu.data.model.home + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CertifyMissionRequest( + val model: String = "gpt-4-vision-preview", + val messages: List, + @SerialName("max_tokens") + val maxTokens: Int = 300, +) { + @Serializable + data class Message( + val role: String, + @SerialName("content") + val contents: List, + ) { + sealed interface Content { + val type: String + + @Serializable + data class TextContent( + val text: String, + override val type: String = "text", + ) : Content + + @Serializable + data class ImageContent( + @SerialName("image_url") + val imageUrl: ImageUrl, + val text: String, + override val type: String = "image_url", + ) : Content { + @Serializable + data class ImageUrl( + val url: String, + ) + } + } + } +} From b64985e91ec0885ef0bf16253710157ad2222bfb Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 01:32:59 +0900 Subject: [PATCH 02/23] feat: implement CertifyMissionResponse --- .../data/model/home/CertifyMissionResponse.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionResponse.kt diff --git a/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionResponse.kt b/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionResponse.kt new file mode 100644 index 0000000..ef92eb9 --- /dev/null +++ b/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionResponse.kt @@ -0,0 +1,51 @@ +package univ.earthbreaker.namu.data.model.home + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CertifyMissionResponse( + val id: String, + @SerialName("object") val objectType: String = "chat.completion", + val created: Int, + val model: String, + @SerialName("system_fingerprint") val systemFingerprint: String, + val choices: List, + val usage: Usage, +) { + @Serializable + data class Choice( + val index: Int, + val message: Message, + @SerialName("finish_reason") val finishReason: String, + val logprobs: Content?, + ) { + @Serializable + data class Message( + val role: String, + val content: String?, + ) + + @Serializable + data class Content( + val token: String, + val logprob: Int, + val bytes: List?, + val top_logprobs: List, + ) { + @Serializable + data class LogProb( + val token: String, + val logprob: Int, + val bytes: List?, + ) + } + } + + @Serializable + data class Usage( + @SerialName("completion_tokens") val completionTokens: Int, + @SerialName("prompt_tokens") val promptTokens: Int, + @SerialName("total_tokens") val totalTokens: Int, + ) +} From 7664235b14949c44ad49107d26c1549ba2fbad8f Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 01:41:03 +0900 Subject: [PATCH 03/23] chore: change local.properties keys --- app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt b/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt index 80f9cd8..c67750e 100644 --- a/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt +++ b/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt @@ -26,7 +26,7 @@ object ApiFactory { } val retrofitForOpenAiService: Retrofit by lazy { - Retrofit.Builder().baseUrl(BuildConfig.OPEN_AI_BASE_URL).client(client) + Retrofit.Builder().baseUrl(BuildConfig.OPENAI_BASE_URL).client(client) .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())).build() } From df431e7a43ecb211efb82c9ac895da17ae1790e4 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 01:41:16 +0900 Subject: [PATCH 04/23] feat: implement OpenAiApiService --- .../namu/data/service/OpenAiApiService.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/src/main/java/univ/earthbreaker/namu/data/service/OpenAiApiService.kt diff --git a/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAiApiService.kt b/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAiApiService.kt new file mode 100644 index 0000000..002c0f6 --- /dev/null +++ b/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAiApiService.kt @@ -0,0 +1,17 @@ +package univ.earthbreaker.namu.data.service + +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST +import univ.earthbreaker.namu.BuildConfig + +interface OpenAiApiService { + @POST("chat/completions") + suspend fun certifyMission( + @Header("Authorization") token: String = "Bearer ${BuildConfig.OPENAI_API_KEY}", + @Body requestBody: RequestBody, + ): Response +} From 0889b556b18570378b2d7ea71a8b7ccf0d2ea580 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 01:43:16 +0900 Subject: [PATCH 05/23] chore: rename OpenAi to OpenAI --- .../data/service/{OpenAiApiService.kt => OpenAIApiService.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/src/main/java/univ/earthbreaker/namu/data/service/{OpenAiApiService.kt => OpenAIApiService.kt} (94%) diff --git a/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAiApiService.kt b/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAIApiService.kt similarity index 94% rename from app/src/main/java/univ/earthbreaker/namu/data/service/OpenAiApiService.kt rename to app/src/main/java/univ/earthbreaker/namu/data/service/OpenAIApiService.kt index 002c0f6..1102488 100644 --- a/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAiApiService.kt +++ b/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAIApiService.kt @@ -8,7 +8,7 @@ import retrofit2.http.Header import retrofit2.http.POST import univ.earthbreaker.namu.BuildConfig -interface OpenAiApiService { +interface OpenAIApiService { @POST("chat/completions") suspend fun certifyMission( @Header("Authorization") token: String = "Bearer ${BuildConfig.OPENAI_API_KEY}", From 4e443cecf8154e4cb4df11abe4489641445580c7 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 01:43:36 +0900 Subject: [PATCH 06/23] feat: add openAIApiService into ServicePool --- .../main/java/univ/earthbreaker/namu/data/ApiFactory.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt b/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt index c67750e..6cc052e 100644 --- a/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt +++ b/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt @@ -7,6 +7,7 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import univ.earthbreaker.namu.BuildConfig +import univ.earthbreaker.namu.data.service.OpenAIApiService object ApiFactory { private val client: OkHttpClient by lazy { @@ -33,13 +34,15 @@ object ApiFactory { inline fun create(forWhichServer: Server): T = when (forWhichServer) { Server.GrowTreeServer -> retrofitForGrowTreeServer.create(T::class.java) - Server.ChatGPT -> retrofitForOpenAiService.create(T::class.java) + Server.OpenAI -> retrofitForOpenAiService.create(T::class.java) } } enum class Server { GrowTreeServer, - ChatGPT, + OpenAI, } -object ServicePool +object ServicePool { + val openAIApiService: OpenAIApiService = ApiFactory.create(forWhichServer = Server.OpenAI) +} From 1bf58761da9c51bb6ca66f729e2a5431cfcc8c0d Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:44:04 +0900 Subject: [PATCH 07/23] feat: update CertifyMissionRequest --- .../data/model/home/CertifyMissionRequest.kt | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionRequest.kt b/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionRequest.kt index 6579f2c..fa5fa8a 100644 --- a/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionRequest.kt +++ b/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionRequest.kt @@ -7,36 +7,23 @@ import kotlinx.serialization.Serializable data class CertifyMissionRequest( val model: String = "gpt-4-vision-preview", val messages: List, - @SerialName("max_tokens") - val maxTokens: Int = 300, + @SerialName("max_tokens") val maxTokens: Int = 300, ) { @Serializable data class Message( - val role: String, - @SerialName("content") - val contents: List, + val role: String = "user", + @SerialName("content") val contents: List, ) { - sealed interface Content { - val type: String - - @Serializable - data class TextContent( - val text: String, - override val type: String = "text", - ) : Content - + @Serializable + data class ImageContent( + @SerialName("image_url") val imageUrl: ImageUrl, + val prompt: String, + @SerialName("type") val contentType: String = "image_url", + ) { @Serializable - data class ImageContent( - @SerialName("image_url") - val imageUrl: ImageUrl, - val text: String, - override val type: String = "image_url", - ) : Content { - @Serializable - data class ImageUrl( - val url: String, - ) - } + data class ImageUrl( + val url: String, + ) } } } From c5049988725901e0c1da4073c244254abfe09b63 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:44:49 +0900 Subject: [PATCH 08/23] feat: add custom Json instance --- .../java/univ/earthbreaker/namu/data/ApiFactory.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt b/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt index 6cc052e..9c79d77 100644 --- a/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt +++ b/app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt @@ -10,6 +10,12 @@ import univ.earthbreaker.namu.BuildConfig import univ.earthbreaker.namu.data.service.OpenAIApiService object ApiFactory { + private val json: Json by lazy { + Json { + encodeDefaults = true + } + } + private val client: OkHttpClient by lazy { OkHttpClient.Builder().addInterceptor( HttpLoggingInterceptor().apply { @@ -22,13 +28,13 @@ object ApiFactory { val retrofitForGrowTreeServer: Retrofit by lazy { Retrofit.Builder().baseUrl(BuildConfig.GROW_TREE_BASE_URL).client(client) .addConverterFactory( - Json.asConverterFactory("application/json".toMediaType()), + json.asConverterFactory("application/json".toMediaType()), ).build() } val retrofitForOpenAiService: Retrofit by lazy { Retrofit.Builder().baseUrl(BuildConfig.OPENAI_BASE_URL).client(client) - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())).build() + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())).build() } inline fun create(forWhichServer: Server): T = From f3c6f79e4a4b6fbfdb0a03847105c4b53cb579fe Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:45:04 +0900 Subject: [PATCH 09/23] feat: implement CertifyMissionResponse toEntity() --- .../namu/data/model/home/CertifyMissionResponse.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionResponse.kt b/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionResponse.kt index ef92eb9..b7fbc42 100644 --- a/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionResponse.kt +++ b/app/src/main/java/univ/earthbreaker/namu/data/model/home/CertifyMissionResponse.kt @@ -2,6 +2,7 @@ package univ.earthbreaker.namu.data.model.home import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import univ.earthbreaker.namu.domain.entity.mission.MissionCertificationInfo @Serializable data class CertifyMissionResponse( @@ -48,4 +49,7 @@ data class CertifyMissionResponse( @SerialName("prompt_tokens") val promptTokens: Int, @SerialName("total_tokens") val totalTokens: Int, ) + + fun toEntity(): MissionCertificationInfo = + MissionCertificationInfo(isSuccessful = this.choices.first().message.content.toBoolean()) } From b719b87590d138ff418704675be4ebb48bbe7511 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:45:20 +0900 Subject: [PATCH 10/23] chore: update HomeActions --- .../namu/feature/home/model/HomeActions.kt | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeActions.kt b/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeActions.kt index 738ae63..25904cc 100644 --- a/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeActions.kt +++ b/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeActions.kt @@ -1,18 +1,10 @@ package univ.earthbreaker.namu.feature.home.model -import android.net.Uri - data class HomeActions( val onClickCameraIcon: () -> Unit, val openDialog: (homeDialogState: HomeDialogState) -> Unit, val dismissDialog: () -> Unit, val levelUpCharacter: () -> Unit, val onMissionDescriptionChanged: (missionDescription: String) -> Unit, - val onPickImage: (uri: Uri?) -> Unit, - val missionCertificationActions: MissionCertificationActions, -) { - data class MissionCertificationActions( - val certifyMission: () -> Unit, - val uploadMission: () -> Unit, - ) -} + val certifyMission: () -> Unit, +) From 9c3309f4c6c5b17b866a6289f2d4f0356348427d Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:45:57 +0900 Subject: [PATCH 11/23] chore: update PickMedia callback --- .../java/univ/earthbreaker/namu/feature/home/HomeActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeActivity.kt b/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeActivity.kt index 3e46507..86ae55e 100644 --- a/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeActivity.kt +++ b/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeActivity.kt @@ -9,11 +9,12 @@ import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia import androidx.activity.viewModels import univ.earthbreaker.namu.ui.theme.GTTheme +import univ.earthbreaker.namu.util.getBase64FromUri class HomeActivity : ComponentActivity() { private val viewModel: HomeViewModel by viewModels() private val pickMedia = registerForActivityResult(PickVisualMedia()) { uri -> - viewModel.setMissionCertificationUri(imageUri = uri) + viewModel.setMissionImage(imageUri = uri, base64 = getBase64FromUri(uri = uri)) } override fun onCreate(savedInstanceState: Bundle?) { From 6c65e8d92b338dad63cbca24ee15b07ca16aebe6 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:46:09 +0900 Subject: [PATCH 12/23] chore: add new HomeDialogState --- .../univ/earthbreaker/namu/feature/home/model/HomeDialogState.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeDialogState.kt b/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeDialogState.kt index db990ae..410b1a2 100644 --- a/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeDialogState.kt +++ b/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeDialogState.kt @@ -4,4 +4,5 @@ data class HomeDialogState( val doesBookDialogExist: Boolean = false, val doesMissionListDialogExist: Boolean = false, val doesMissionCertificationDialogExist: Boolean = false, + val doesMissionCertificationResultDialogExist: Boolean = false, ) From 84418c7948bd242d98dd3e719b88f0b9ad20d20b Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:46:17 +0900 Subject: [PATCH 13/23] chore: update actions --- .../namu/feature/home/HomeRoute.kt | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeRoute.kt b/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeRoute.kt index c230d48..c841817 100644 --- a/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeRoute.kt +++ b/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeRoute.kt @@ -38,19 +38,11 @@ fun rememberHomeActions(viewModel: HomeViewModel, onClickCameraIcon: () -> Unit) return remember(viewModel) { HomeActions( onClickCameraIcon = onClickCameraIcon, - openDialog = { homeDialogState -> viewModel.openDialog(homeDialogState = homeDialogState) }, - dismissDialog = { viewModel.dismissDialog() }, - levelUpCharacter = { viewModel.levelUpCharacter() }, - onMissionDescriptionChanged = { newMissionDescription -> - viewModel.onMissionDescriptionChanged( - newMissionDescription = newMissionDescription, - ) - }, - onPickImage = { uri -> viewModel.setMissionCertificationUri(uri) }, - missionCertificationActions = HomeActions.MissionCertificationActions( - certifyMission = { viewModel.certifyMission() }, - uploadMission = { viewModel.uploadMission() }, - ), + openDialog = viewModel::openDialog, + dismissDialog = viewModel::dismissDialog, + levelUpCharacter = viewModel::levelUpCharacter, + onMissionDescriptionChanged = viewModel::onMissionDescriptionChanged, + certifyMission = viewModel::certifyMission, ) } } From 48bd22510f8bc068a75e545be531f6c427e52b9c Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:46:47 +0900 Subject: [PATCH 14/23] feat: add MissionCertificationResultDialog --- .../namu/feature/home/HomeScreen.kt | 19 +++++++++----- .../MissionCertificationResultDialog.kt | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionCertificationResultDialog.kt diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeScreen.kt b/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeScreen.kt index 8b10739..54f40d0 100644 --- a/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeScreen.kt +++ b/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeScreen.kt @@ -37,6 +37,7 @@ import univ.earthbreaker.namu.feature.home.model.HomeDialogState import univ.earthbreaker.namu.feature.home.model.HomeUiState import univ.earthbreaker.namu.feature.home.type.CharacterLevel import univ.earthbreaker.namu.feature.mission.MissionCertificationDialog +import univ.earthbreaker.namu.feature.mission.MissionCertificationResultDialog import univ.earthbreaker.namu.feature.mission.MissionListDialog import univ.earthbreaker.namu.ui.theme.GTTheme import univ.earthbreaker.namu.ui.theme.navigationBarHeight @@ -118,10 +119,17 @@ fun HomeScreen( }, missionName = "비건 카페 방문하기", missionDescription = state.missionDescription, - missionImageUri = state.missionCertificationUri, + missionImageUri = state.missionImageUri, onClickCameraImage = actions.onClickCameraIcon, onMissionDescriptionChanged = actions.onMissionDescriptionChanged, - uploadMission = actions.missionCertificationActions.uploadMission, + certifyMission = actions.certifyMission, + ) + } + + if (state.homeDialogState.doesMissionCertificationResultDialogExist) { + MissionCertificationResultDialog( + isSuccessful = state.isMissionCertificationSuccessful ?: false, + onDismissRequest = {}, ) } } @@ -150,7 +158,7 @@ fun HomeTopCard( .padding(start = 7.dp) .height(11.dp) .clip(RoundedCornerShape(20.dp)), - progress = userExp, + progress = { userExp }, color = GTTheme.colors.green1, trackColor = GTTheme.colors.bg2Black, ) @@ -236,7 +244,7 @@ private fun HomeScreenPreview() { HomeScreen( state = HomeUiState.Success( HomeInfo(), - missionCertificationUri = null, + missionImageUri = null, missionDescription = "", ), actions = HomeActions( @@ -245,8 +253,7 @@ private fun HomeScreenPreview() { dismissDialog = {}, levelUpCharacter = {}, onMissionDescriptionChanged = {}, - onPickImage = {}, - HomeActions.MissionCertificationActions(certifyMission = {}, uploadMission = {}), + certifyMission = {}, ), ) } diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionCertificationResultDialog.kt b/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionCertificationResultDialog.kt new file mode 100644 index 0000000..d635e0d --- /dev/null +++ b/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionCertificationResultDialog.kt @@ -0,0 +1,26 @@ +package univ.earthbreaker.namu.feature.mission + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog + +@Composable +fun MissionCertificationResultDialog(isSuccessful: Boolean, onDismissRequest: () -> Unit) { + Dialog(onDismissRequest = onDismissRequest) { + Column { + Text(text = if (isSuccessful) "미션 인증에 성공했습니다." else "미션 인증에 실패했습니다.") + Button(onClick = onDismissRequest) { + Text("확인") + } + } + } +} + +@Preview +@Composable +private fun MissionCertificationResultDialogDialogPreview() { + MissionCertificationResultDialog(onDismissRequest = {}, isSuccessful = true) +} From 7019acbc5e7e1e8009714cce44c1380d15558301 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:47:05 +0900 Subject: [PATCH 15/23] feat: add MissionRepository --- .../mission/MissionRepositoryImpl.kt | 32 +++++++++++++++++++ .../domain/repository/MissionRepository.kt | 10 ++++++ 2 files changed, 42 insertions(+) create mode 100644 app/src/main/java/univ/earthbreaker/namu/data/repository/mission/MissionRepositoryImpl.kt create mode 100644 app/src/main/java/univ/earthbreaker/namu/domain/repository/MissionRepository.kt diff --git a/app/src/main/java/univ/earthbreaker/namu/data/repository/mission/MissionRepositoryImpl.kt b/app/src/main/java/univ/earthbreaker/namu/data/repository/mission/MissionRepositoryImpl.kt new file mode 100644 index 0000000..e262a1c --- /dev/null +++ b/app/src/main/java/univ/earthbreaker/namu/data/repository/mission/MissionRepositoryImpl.kt @@ -0,0 +1,32 @@ +package univ.earthbreaker.namu.data.repository.mission + +import univ.earthbreaker.namu.data.ServicePool +import univ.earthbreaker.namu.data.model.home.CertifyMissionRequest +import univ.earthbreaker.namu.data.model.home.CertifyMissionRequest.Message +import univ.earthbreaker.namu.data.model.home.CertifyMissionRequest.Message.ImageContent +import univ.earthbreaker.namu.data.model.home.CertifyMissionRequest.Message.ImageContent.ImageUrl +import univ.earthbreaker.namu.domain.entity.mission.MissionCertificationInfo +import univ.earthbreaker.namu.domain.repository.MissionRepository + +class MissionRepositoryImpl : MissionRepository { + override suspend fun certifyMission( + prompt: String, + imageBase64: String, + ): MissionCertificationInfo = + ServicePool.openAIApiService.certifyMission( + certifyMissionRequest = CertifyMissionRequest( + messages = listOf( + Message( + contents = listOf( + ImageContent( + imageUrl = ImageUrl( + url = imageBase64, + ), + prompt = prompt, + ), + ), + ), + ), + ), + ).toEntity() +} diff --git a/app/src/main/java/univ/earthbreaker/namu/domain/repository/MissionRepository.kt b/app/src/main/java/univ/earthbreaker/namu/domain/repository/MissionRepository.kt new file mode 100644 index 0000000..89b400e --- /dev/null +++ b/app/src/main/java/univ/earthbreaker/namu/domain/repository/MissionRepository.kt @@ -0,0 +1,10 @@ +package univ.earthbreaker.namu.domain.repository + +import univ.earthbreaker.namu.domain.entity.mission.MissionCertificationInfo + +interface MissionRepository { + suspend fun certifyMission( + prompt: String, + imageBase64: String, + ): MissionCertificationInfo +} From 47a96289c940e626e0732b35db9be0a2d12c141e Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:47:12 +0900 Subject: [PATCH 16/23] feat: add ContextExtension --- .../earthbreaker/namu/util/ContextExtension.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/src/main/java/univ/earthbreaker/namu/util/ContextExtension.kt diff --git a/app/src/main/java/univ/earthbreaker/namu/util/ContextExtension.kt b/app/src/main/java/univ/earthbreaker/namu/util/ContextExtension.kt new file mode 100644 index 0000000..1b6b42c --- /dev/null +++ b/app/src/main/java/univ/earthbreaker/namu/util/ContextExtension.kt @@ -0,0 +1,17 @@ +package univ.earthbreaker.namu.util + +import android.content.Context +import android.net.Uri +import android.util.Base64 +import java.io.IOException + +fun Context.getBase64FromUri(uri: Uri?): String? { + try { + val bytes = + contentResolver.openInputStream(uri ?: throw NullPointerException())?.readBytes() + return Base64.encodeToString(bytes, Base64.DEFAULT) + } catch (error: IOException) { + error.printStackTrace() // This exception always occurs + } + return null +} From f3b2d89bd7b679a053e25aef3bab5324c4d21ce8 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:47:19 +0900 Subject: [PATCH 17/23] feat: add MissionCertificationType --- .../namu/feature/mission/type/MissionCertificationType.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/main/java/univ/earthbreaker/namu/feature/mission/type/MissionCertificationType.kt diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/mission/type/MissionCertificationType.kt b/app/src/main/java/univ/earthbreaker/namu/feature/mission/type/MissionCertificationType.kt new file mode 100644 index 0000000..4d65b0e --- /dev/null +++ b/app/src/main/java/univ/earthbreaker/namu/feature/mission/type/MissionCertificationType.kt @@ -0,0 +1,8 @@ +package univ.earthbreaker.namu.feature.mission.type + +enum class MissionCertificationType { + NOT_YET, + REJECTED, + CERTIFIED, + CONNECTION_FAIL, +} From 1650b37d87623fdb1a28eba4b8f42cb67a9af0e9 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:47:24 +0900 Subject: [PATCH 18/23] feat: add MissionCertificationInfo --- .../namu/domain/entity/mission/MissionCertificationInfo.kt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/MissionCertificationInfo.kt diff --git a/app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/MissionCertificationInfo.kt b/app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/MissionCertificationInfo.kt new file mode 100644 index 0000000..46b6f08 --- /dev/null +++ b/app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/MissionCertificationInfo.kt @@ -0,0 +1,3 @@ +package univ.earthbreaker.namu.domain.entity.mission + +data class MissionCertificationInfo(val isSuccessful: Boolean) From 81d618de624965229411afe715b60740f893d43a Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:47:40 +0900 Subject: [PATCH 19/23] feat: implement CertifyMissionUseCase --- .../usecase/mission/CertifyMissionUseCase.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/src/main/java/univ/earthbreaker/namu/domain/usecase/mission/CertifyMissionUseCase.kt diff --git a/app/src/main/java/univ/earthbreaker/namu/domain/usecase/mission/CertifyMissionUseCase.kt b/app/src/main/java/univ/earthbreaker/namu/domain/usecase/mission/CertifyMissionUseCase.kt new file mode 100644 index 0000000..622b63b --- /dev/null +++ b/app/src/main/java/univ/earthbreaker/namu/domain/usecase/mission/CertifyMissionUseCase.kt @@ -0,0 +1,26 @@ +package univ.earthbreaker.namu.domain.usecase.mission + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import univ.earthbreaker.namu.data.repository.mission.MissionRepositoryImpl +import univ.earthbreaker.namu.domain.entity.mission.MissionCertificationInfo +import univ.earthbreaker.namu.domain.repository.MissionRepository + +class CertifyMissionUseCase(private val missionRepository: MissionRepository = MissionRepositoryImpl()) { + suspend operator fun invoke(imageBase64: String): Flow = + flow { + emit( + missionRepository.certifyMission( + imageBase64 = imageBase64, + prompt = """you're a helpful assistant who classify image by true and false. + if an image is related to practicing environmental conservation, + answer true. else false. + """.trimIndent(), + ), + ) + }.flowOn( + Dispatchers.IO, + ) +} From 812243910e7f61362b97c4b409adfdaf62dd528f Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:48:19 +0900 Subject: [PATCH 20/23] feat: add new state for HomeUiState.SUCCESS --- .../univ/earthbreaker/namu/feature/home/model/HomeUiState.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeUiState.kt b/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeUiState.kt index 81129fa..80a1c7c 100644 --- a/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeUiState.kt +++ b/app/src/main/java/univ/earthbreaker/namu/feature/home/model/HomeUiState.kt @@ -9,7 +9,8 @@ sealed interface HomeUiState { data class Success( val homeInfo: HomeInfo, val missionDescription: String, - val missionCertificationUri: Uri?, + val missionImageUri: Uri?, + val isMissionCertificationSuccessful: Boolean? = null, val homeDialogState: HomeDialogState = HomeDialogState(), ) : HomeUiState From deb3bb9d8c7c8ccbdf6e008efbe88cd6eded3f72 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:48:26 +0900 Subject: [PATCH 21/23] chore: rename --- .../mission/{MissionItem.kt => MissionInfo.kt} | 2 +- .../namu/feature/mission/MissionListDialog.kt | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) rename app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/{MissionItem.kt => MissionInfo.kt} (83%) diff --git a/app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/MissionItem.kt b/app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/MissionInfo.kt similarity index 83% rename from app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/MissionItem.kt rename to app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/MissionInfo.kt index 52cd70f..871e80a 100644 --- a/app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/MissionItem.kt +++ b/app/src/main/java/univ/earthbreaker/namu/domain/entity/mission/MissionInfo.kt @@ -1,6 +1,6 @@ package univ.earthbreaker.namu.domain.entity.mission -data class MissionItem( +data class MissionInfo( val missionName: String, val beforeChallenge: Boolean, ) diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionListDialog.kt b/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionListDialog.kt index 6cf3422..de8ad63 100644 --- a/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionListDialog.kt +++ b/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionListDialog.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import univ.earthbreaker.namu.R import univ.earthbreaker.namu.compose.CommonDialogWithXIcon -import univ.earthbreaker.namu.domain.entity.mission.MissionItem +import univ.earthbreaker.namu.domain.entity.mission.MissionInfo import univ.earthbreaker.namu.feature.mission.component.MissionListBody import univ.earthbreaker.namu.feature.mission.component.MissionListHeader import univ.earthbreaker.namu.ui.theme.GTTheme @@ -25,15 +25,15 @@ fun MissionListDialog( onDismissRequest: () -> Unit, onClickChallenge: () -> Unit, ) { - val mockList1: List = listOf( - MissionItem(missionName = "비건 카페 방문하기", beforeChallenge = true), - MissionItem(missionName = "대중 교통 이용하기", beforeChallenge = false), + val mockList1: List = listOf( + MissionInfo(missionName = "비건 카페 방문하기", beforeChallenge = true), + MissionInfo(missionName = "대중 교통 이용하기", beforeChallenge = false), ) - val mockList2: List = listOf( - MissionItem(missionName = "텀블러 사용하기", beforeChallenge = true), - MissionItem(missionName = "분리수거 하기", beforeChallenge = true), - MissionItem(missionName = "다회용기로 포장하기", beforeChallenge = true), + val mockList2: List = listOf( + MissionInfo(missionName = "텀블러 사용하기", beforeChallenge = true), + MissionInfo(missionName = "분리수거 하기", beforeChallenge = true), + MissionInfo(missionName = "다회용기로 포장하기", beforeChallenge = true), ) CommonDialogWithXIcon( From 22ac898d0cead2ea60b59f8ad1a1373b56af9ebf Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:49:02 +0900 Subject: [PATCH 22/23] chore: update certifyMission --- .../earthbreaker/namu/data/service/OpenAIApiService.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAIApiService.kt b/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAIApiService.kt index 1102488..43e2d19 100644 --- a/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAIApiService.kt +++ b/app/src/main/java/univ/earthbreaker/namu/data/service/OpenAIApiService.kt @@ -1,17 +1,16 @@ package univ.earthbreaker.namu.data.service -import okhttp3.RequestBody -import okhttp3.ResponseBody -import retrofit2.Response import retrofit2.http.Body import retrofit2.http.Header import retrofit2.http.POST import univ.earthbreaker.namu.BuildConfig +import univ.earthbreaker.namu.data.model.home.CertifyMissionRequest +import univ.earthbreaker.namu.data.model.home.CertifyMissionResponse interface OpenAIApiService { @POST("chat/completions") suspend fun certifyMission( @Header("Authorization") token: String = "Bearer ${BuildConfig.OPENAI_API_KEY}", - @Body requestBody: RequestBody, - ): Response + @Body certifyMissionRequest: CertifyMissionRequest, + ): CertifyMissionResponse } From 541394d844da9b9ac9898d8b5f45e8b9d99fc334 Mon Sep 17 00:00:00 2001 From: giovannijunseokim Date: Tue, 2 Apr 2024 04:49:23 +0900 Subject: [PATCH 23/23] chore: implement certifyMission logic --- .../namu/feature/home/HomeViewModel.kt | 75 ++++++++++++++++--- .../mission/MissionCertificationDialog.kt | 6 +- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeViewModel.kt b/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeViewModel.kt index e9880c8..89cf644 100644 --- a/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeViewModel.kt +++ b/app/src/main/java/univ/earthbreaker/namu/feature/home/HomeViewModel.kt @@ -1,7 +1,6 @@ package univ.earthbreaker.namu.feature.home import android.net.Uri -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow @@ -10,11 +9,14 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import univ.earthbreaker.namu.domain.entity.mission.HomeInfo import univ.earthbreaker.namu.domain.usecase.home.GetHomeInfoUseCase +import univ.earthbreaker.namu.domain.usecase.mission.CertifyMissionUseCase import univ.earthbreaker.namu.feature.home.model.HomeDialogState import univ.earthbreaker.namu.feature.home.model.HomeUiState +import univ.earthbreaker.namu.feature.mission.type.MissionCertificationType class HomeViewModel( private val getHomeInfoUseCase: GetHomeInfoUseCase = GetHomeInfoUseCase(), + private val certifyMissionUseCase: CertifyMissionUseCase = CertifyMissionUseCase(), ) : ViewModel() { init { getHomeInfo() @@ -29,23 +31,25 @@ class HomeViewModel( HomeUiState.Success( homeInfo = homeInfo.value, missionDescription = missionDescription.value, - missionCertificationUri = missionCertificationUri.value, + missionImageUri = missionImageUri.value, homeDialogState = HomeDialogState(doesMissionCertificationDialogExist = true), ), ) } } - private val missionCertificationUri: MutableStateFlow = MutableStateFlow(null) + private val missionImageUri: MutableStateFlow = MutableStateFlow(null) + private var missionImageBase64: String? = null - fun setMissionCertificationUri(imageUri: Uri?) { - missionCertificationUri.value = imageUri + fun setMissionImage(imageUri: Uri?, base64: String?) { + missionImageUri.value = imageUri + missionImageBase64 = base64 viewModelScope.launch { _homeUiStateFlow.emit( HomeUiState.Success( homeInfo = homeInfo.value, missionDescription = missionDescription.value, - missionCertificationUri = missionCertificationUri.value, + missionImageUri = missionImageUri.value, homeDialogState = HomeDialogState(doesMissionCertificationDialogExist = true), ), ) @@ -54,12 +58,14 @@ class HomeViewModel( private val homeInfo: MutableStateFlow = MutableStateFlow(HomeInfo()) + private var missionCertificationState: MutableStateFlow = + MutableStateFlow(MissionCertificationType.NOT_YET) + private val _homeUiStateFlow: MutableStateFlow = MutableStateFlow(HomeUiState.Loading) val homeUiStateFlow: StateFlow = _homeUiStateFlow.asStateFlow() private fun getHomeInfo() { - Log.e("gio", "getHomeInfo invoked...") viewModelScope.launch { val getHomeInfoFlow = getHomeInfoUseCase() getHomeInfoFlow.collect { @@ -68,10 +74,9 @@ class HomeViewModel( HomeUiState.Success( it, missionDescription = missionDescription.value, - missionCertificationUri = missionCertificationUri.value, + missionImageUri = missionImageUri.value, ), ) - Log.e("gio", "${_homeUiStateFlow.value}") } } } @@ -82,7 +87,7 @@ class HomeViewModel( HomeUiState.Success( homeInfo = homeInfo.value, missionDescription = missionDescription.value, - missionCertificationUri = missionCertificationUri.value, + missionImageUri = missionImageUri.value, homeDialogState = homeDialogState, ), ) @@ -95,7 +100,7 @@ class HomeViewModel( HomeUiState.Success( homeInfo = homeInfo.value, missionDescription = missionDescription.value, - missionCertificationUri = missionCertificationUri.value, + missionImageUri = missionImageUri.value, homeDialogState = HomeDialogState(), ), ) @@ -106,8 +111,54 @@ class HomeViewModel( } fun certifyMission() { + viewModelScope.launch { + _homeUiStateFlow.emit(HomeUiState.Loading) + runCatching { + certifyMissionUseCase( + imageBase64 = missionImageBase64 ?: throw Exception(), + ) + }.fold( + onSuccess = { flow -> + flow.collect { missionCertificationInfo -> + if (missionCertificationInfo.isSuccessful) { + uploadMission() + } else { + missionCertificationState.value = MissionCertificationType.REJECTED + _homeUiStateFlow.emit( + HomeUiState.Success( + homeInfo = homeInfo.value, + missionDescription = missionDescription.value, + missionImageUri = missionImageUri.value, + homeDialogState = HomeDialogState( + doesMissionListDialogExist = true, + doesMissionCertificationResultDialogExist = true, + ), + ), + ) + } + } + }, + onFailure = { + missionCertificationState.value = MissionCertificationType.CONNECTION_FAIL + }, + ) + } } - fun uploadMission() { + private fun uploadMission() { + missionCertificationState.value = MissionCertificationType.CERTIFIED + viewModelScope.launch { + _homeUiStateFlow.emit( + HomeUiState.Success( + homeInfo = homeInfo.value, + missionDescription = missionDescription.value, + missionImageUri = missionImageUri.value, + homeDialogState = HomeDialogState( + doesMissionListDialogExist = true, + doesMissionCertificationResultDialogExist = true, + ), + ), + ) + } } } diff --git a/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionCertificationDialog.kt b/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionCertificationDialog.kt index f694960..64fc1b5 100644 --- a/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionCertificationDialog.kt +++ b/app/src/main/java/univ/earthbreaker/namu/feature/mission/MissionCertificationDialog.kt @@ -40,7 +40,7 @@ fun MissionCertificationDialog( missionImageUri: Uri?, onMissionDescriptionChanged: (missionDescription: String) -> Unit, onClickCameraImage: () -> Unit, - uploadMission: () -> Unit, + certifyMission: () -> Unit, ) { CommonDialogWithXIcon(onDismissRequest = onDismissRequest) { Column( @@ -136,7 +136,7 @@ fun MissionCertificationDialog( ) .padding(vertical = 11.dp, horizontal = 49.dp) .clickable { - uploadMission() + certifyMission() }, color = GTTheme.colors.white, style = GTTheme.typography.detailMedium14, @@ -157,6 +157,6 @@ private fun MissionCertificationDialogPreview() { missionImageUri = null, onMissionDescriptionChanged = { missionDescription = it }, onClickCameraImage = {}, - uploadMission = {}, + certifyMission = {}, ) }