Skip to content
This repository has been archived by the owner on Oct 20, 2024. It is now read-only.

Commit

Permalink
📚 외부 일기 불러오기 기능 (#88)
Browse files Browse the repository at this point in the history
* [feat] 일기 가져오기 기능 api 작업

* [style] home 화면 버튼 위치 수정
  • Loading branch information
HamBeomJoon authored Aug 13, 2024
1 parent 3e4a08a commit e938b23
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package com.nabi.data.datasource

import com.nabi.data.model.BaseResponse
import com.nabi.data.model.MessageResponseDTO
import com.nabi.data.model.user.UserInfoResponseDTO
import java.io.InputStream

interface UserRemoteDataSource {

suspend fun getUserInfo(
accessToken: String,
): Result<BaseResponse<UserInfoResponseDTO>>

suspend fun loadDiary(
accessToken: String,
realPath: String
): Result<BaseResponse<MessageResponseDTO>>
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package com.nabi.data.datasourceImpl

import android.util.Log
import com.nabi.data.datasource.UserRemoteDataSource
import com.nabi.data.model.BaseResponse
import com.nabi.data.model.MessageResponseDTO
import com.nabi.data.model.user.UserInfoResponseDTO
import com.nabi.data.service.UserService
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import org.json.JSONObject
import java.io.File
import javax.inject.Inject

class UserRemoteDataSourceImpl @Inject constructor(
Expand All @@ -29,4 +38,26 @@ class UserRemoteDataSourceImpl @Inject constructor(
Result.failure(e)
}
}

override suspend fun loadDiary(
accessToken: String,
realPath: String
): Result<BaseResponse<MessageResponseDTO>> {
return try {
val file = File(realPath)
val requestFile = file.asRequestBody("application/pdf".toMediaTypeOrNull())
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)

val res = userService.loadDiary(accessToken, body)
if (res.isSuccessful) Result.success(res.body()!!)
else {
val errorBody = JSONObject(res.errorBody()?.string()!!)
Result.failure(Exception("Load Diary failed: ${errorBody.getString("message")}"))
}

} catch (e: Exception) {
Log.e("TAG", e.stackTraceToString())
Result.failure(e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.nabi.data.repository
import com.nabi.data.datasource.UserRemoteDataSource
import com.nabi.domain.model.user.UserInfo
import com.nabi.domain.repository.UserRepository
import java.io.InputStream
import javax.inject.Inject

class UserRepositoryImpl @Inject constructor(
Expand All @@ -28,4 +29,24 @@ class UserRepositoryImpl @Inject constructor(
Result.failure(result.exceptionOrNull() ?: Exception("Unknown error"))
}
}

override suspend fun loadDiary(accessToken: String, realPath: String): Result<String> {
val result = userRemoteDataSource.loadDiary(accessToken, realPath)

return if (result.isSuccess) {
val res = result.getOrNull()
if (res != null) {
val data = res.data
if (data != null) {
Result.success(data.message)
} else {
Result.failure(Exception("Load Diary failed: data is null"))
}
} else {
Result.failure(Exception("Load Diary failed: response body is null"))
}
} else {
Result.failure(result.exceptionOrNull() ?: Exception("Unknown error"))
}
}
}
12 changes: 12 additions & 0 deletions Nabi/data/src/main/java/com/nabi/data/service/UserService.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
package com.nabi.data.service

import com.nabi.data.model.BaseResponse
import com.nabi.data.model.MessageResponseDTO
import com.nabi.data.model.user.UserInfoResponseDTO
import okhttp3.MultipartBody
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part

interface UserService {

@GET("/user-info")
suspend fun getUserInfo(
@Header("Authorization") accessToken: String,
): Response<BaseResponse<UserInfoResponseDTO>>

@Multipart
@POST("/users/pdf")
suspend fun loadDiary(
@Header("Authorization") accessToken: String,
@Part file: MultipartBody.Part
): Response<BaseResponse<MessageResponseDTO>>
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.nabi.domain.repository

import com.nabi.domain.model.user.UserInfo
import java.io.InputStream


interface UserRepository {
suspend fun getUserInfo(accessToken: String): Result<UserInfo>

suspend fun loadDiary(accessToken: String, realPath: String): Result<String>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.nabi.domain.usecase.user

import com.nabi.domain.repository.UserRepository
import java.io.InputStream

class LoadDiaryUseCase(private val repository: UserRepository) {
suspend operator fun invoke(accessToken: String, realPath: String): Result<String> {
return repository.loadDiary("Bearer $accessToken", realPath)
}
}
6 changes: 6 additions & 0 deletions Nabi/presentation/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<!-- Devices running Android 12L (API level 32) or lower -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

<!-- Devices running Android 13 (API level 33) or higher -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:ignore="SelectedPhotoAccess" />

<application
android:name=".base.NabiApplication"
android:allowBackup="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.nabi.nabi.di

import com.nabi.domain.repository.UserRepository
import com.nabi.domain.usecase.user.GetUserInfoUseCase
import com.nabi.domain.usecase.user.LoadDiaryUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -19,4 +20,12 @@ object UserUseCaseModule {
): GetUserInfoUseCase {
return GetUserInfoUseCase(repository = repository)
}

@Provides
@Singleton
fun provideLoadDiaryUseCase(
repository: UserRepository
): LoadDiaryUseCase {
return LoadDiaryUseCase(repository = repository)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package com.nabi.nabi.views.myPage

import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.datastore.dataStore
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.viewModels
import com.nabi.nabi.R
import com.nabi.nabi.base.BaseFragment
Expand All @@ -16,15 +23,39 @@ import com.nabi.nabi.views.MainActivity
import com.nabi.nabi.views.sign.SignActivity
import com.nabi.nabi.views.sign.SignInNicknameFragment
import dagger.hilt.android.AndroidEntryPoint
import java.io.File
import java.io.FileOutputStream
import java.io.IOException

@AndroidEntryPoint
class MyPageFragment : BaseFragment<FragmentMypageBinding>(R.layout.fragment_mypage) {
private val myPageViewModel: MyPageViewModel by viewModels()

private lateinit var pdfPickerLauncher: ActivityResultLauncher<Intent>

private val requiredGalleryPermissions =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(Manifest.permission.READ_MEDIA_IMAGES)
} else {
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}

@SuppressLint("SetTextI18n")
override fun initView() {
binding.tvNickname.text = "$nickname"
binding.tvConsecutiveDay.text = "일기 작성 ${consecutiveDay}일 째"

pdfPickerLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
LoggerUtils.d(it.toString())
readPdfFile(it)
}
}
}
}

override fun initListener() {
Expand All @@ -38,6 +69,11 @@ class MyPageFragment : BaseFragment<FragmentMypageBinding>(R.layout.fragment_myp

binding.btnLoadDiary.setOnClickListener {

val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
type = "application/pdf"
addCategory(Intent.CATEGORY_OPENABLE)
}
pdfPickerLauncher.launch(intent)
}

binding.btnWithdraw.setOnClickListener {
Expand All @@ -59,6 +95,20 @@ class MyPageFragment : BaseFragment<FragmentMypageBinding>(R.layout.fragment_myp

override fun setObserver() {
super.setObserver()

myPageViewModel.loadState.observe(viewLifecycleOwner) {
when (it) {
is UiState.Loading -> {}
is UiState.Failure -> {
showToast("일기 가져오기 실패")
}

is UiState.Success -> {
showToast("일기 가져오기 성공!")
}
}
}

myPageViewModel.withdrawState.observe(viewLifecycleOwner) {
when (it) {
is UiState.Loading -> {}
Expand Down Expand Up @@ -89,4 +139,49 @@ class MyPageFragment : BaseFragment<FragmentMypageBinding>(R.layout.fragment_myp
}
}
}

private fun readPdfFile(uri: Uri) {
val realPath = getRealPathFromPdfUri(requireContext(), uri)
if (realPath != null) {
LoggerUtils.d("Real Path: $realPath")
myPageViewModel.loadDiary(realPath)
} else {
LoggerUtils.e("Failed to get real path from URI")
showToast("PDF 파일 경로를 가져오는데 실패했습니다.")
}
}

private fun getRealPathFromPdfUri(context: Context, uri: Uri): String? {
// ContentProvider를 통해 파일 정보 가져오기
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val displayName = if (displayNameIndex != -1) {
cursor.getString(displayNameIndex)
} else {
// 파일 이름을 가져올 수 없는 경우 임의의 이름 생성
"temp_pdf_${System.currentTimeMillis()}.pdf"
}

// 임시 파일 생성
val file = File(context.cacheDir, displayName)

try {
// URI에서 입력 스트림 열기
context.contentResolver.openInputStream(uri)?.use { input ->
// 파일에 출력 스트림 열기
FileOutputStream(file).use { output ->
// 입력 스트림에서 출력 스트림으로 데이터 복사
input.copyTo(output)
}
}
// 임시 파일의 절대 경로 반환
return file.absolutePath
} catch (e: IOException) {
LoggerUtils.e("Error copying file: ${e.message}")
}
}
}
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,52 @@ import androidx.lifecycle.viewModelScope
import com.nabi.domain.usecase.auth.WithdrawUseCase
import com.nabi.domain.usecase.datastore.ClearUserDataUseCase
import com.nabi.domain.usecase.datastore.GetAccessTokenUseCase
import com.nabi.domain.usecase.user.LoadDiaryUseCase
import com.nabi.nabi.utils.LoggerUtils
import com.nabi.nabi.utils.UiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.io.InputStream
import javax.inject.Inject

@HiltViewModel
class MyPageViewModel @Inject constructor(
private val loadDiaryUseCase: LoadDiaryUseCase,
private val withdrawUseCase: WithdrawUseCase,
private val clearUserDataUseCase: ClearUserDataUseCase,
private val getAccessTokenUseCase: GetAccessTokenUseCase
) : ViewModel() {

private var _loadState = MutableLiveData<UiState<String>>(UiState.Loading)
val loadState get() = _loadState

private val _withdrawState = MutableLiveData<UiState<String>>(UiState.Loading)
val withdrawState: LiveData<UiState<String>> get() = _withdrawState

private val _clearState = MutableLiveData<UiState<Boolean>>(UiState.Loading)
val clearState: LiveData<UiState<Boolean>> get() = _clearState

fun loadDiary(realPath: String) {
_loadState.value = UiState.Loading

viewModelScope.launch {
val accessToken = getAccessTokenUseCase.invoke().getOrNull().orEmpty()

try {
loadDiaryUseCase(accessToken, realPath)
.onSuccess {
_loadState.value = UiState.Success(it)
}
.onFailure { e ->
LoggerUtils.e("Load Diary failed: ${e.message}")
_loadState.value = UiState.Failure(message = e.message.toString())
}
} catch (e: Exception) {
LoggerUtils.e("Load Diary exception: ${e.message}")
_loadState.value = UiState.Failure(message = e.message.toString())
}
}
}

fun withdraw() {
_withdrawState.value = UiState.Loading
Expand Down
3 changes: 2 additions & 1 deletion Nabi/presentation/src/main/res/layout/fragment_home.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@
android:background="@color/transparent"
android:contentDescription="@null"
android:src="@drawable/ic_mypage"
android:layout_marginEnd="4dp"

app:layout_constraintBottom_toBottomOf="@id/ib_notification"
app:layout_constraintStart_toEndOf="@id/ib_notification"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/ib_notification" />

<TextView
Expand Down

0 comments on commit e938b23

Please sign in to comment.