From b76a83d42901b5c4040db2b2efad9a0a1828ea28 Mon Sep 17 00:00:00 2001 From: aengzu Date: Thu, 24 Oct 2024 15:44:06 +0900 Subject: [PATCH 01/19] =?UTF-8?q?Refactor:=20NotetakingActivity=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *NotetakingViewModel에 documnetId, FileName, FileUri 관리 *메서드 분리 --- .../iguana/dashBoard/RecentFilesViewModel.kt | 6 +- .../iguana/notetaking/NotetakingActivity.kt | 110 ++++++++++++------ .../iguana/notetaking/NotetakingViewModel.kt | 18 ++- .../notetaking/recording/RecordFragment.kt | 1 + .../src/main/res/layout-sw600dp/titlebar.xml | 5 +- .../main/res/layout/activity_notetaking.xml | 2 - .../src/main/res/layout/titlebar.xml | 5 +- .../src/main/res/values/strings.xml | 1 + 8 files changed, 99 insertions(+), 49 deletions(-) diff --git a/feature/dashBoard/src/main/java/com/iguana/dashBoard/RecentFilesViewModel.kt b/feature/dashBoard/src/main/java/com/iguana/dashBoard/RecentFilesViewModel.kt index 5b7ec2b..b4193f6 100644 --- a/feature/dashBoard/src/main/java/com/iguana/dashBoard/RecentFilesViewModel.kt +++ b/feature/dashBoard/src/main/java/com/iguana/dashBoard/RecentFilesViewModel.kt @@ -57,7 +57,7 @@ class RecentFilesViewModel @Inject constructor( val intent = Intent(context, NotetakingActivity::class.java).apply { putExtra("PDF_URI", recentFile.fileUri) putExtra("PDF_TITLE", recentFile.fileName) - putExtra("DOCUMENT_ID", recentFile.id.toString()) + putExtra("DOCUMENT_ID", recentFile.id) } context.startActivity(intent) } @@ -117,10 +117,10 @@ class RecentFilesViewModel @Inject constructor( // TODO: 서버완료되면 아래 코드 삭제 후 위 코드 주석해제 val intent = Intent(context, NotetakingActivity::class.java).apply { - putExtra("PDF_URI", internalUri) + putExtra("PDF_URI", internalUri.toString()) putExtra("PDF_TITLE", fileName) // 시간으로 더미값 생성해서 넣기 - putExtra("DOCUMENT_ID", System.currentTimeMillis().toString()) + putExtra("DOCUMENT_ID", System.currentTimeMillis()) } context.startActivity(intent) } catch (e: Exception) { diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt index 139c7ef..52ef38f 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt @@ -3,6 +3,7 @@ package com.iguana.notetaking import android.net.Uri import android.os.Bundle import android.util.Log +import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -13,6 +14,15 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class NotetakingActivity : AppCompatActivity() { + + companion object { + const val PDF_URI_KEY = "PDF_URI" + const val PDF_TITLE_KEY = "PDF_TITLE" + const val DEFAULT_TITLE = "무제" + const val DOCUMENT_ID_KEY = "DOCUMENT_ID" + } + + private lateinit var binding: ActivityNotetakingBinding private val viewModel: NotetakingViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { @@ -24,44 +34,78 @@ class NotetakingActivity : AppCompatActivity() { binding.viewModel = viewModel binding.lifecycleOwner = this - // Intent에서 PDF URI 받기 - val pdfUriString = intent.getStringExtra("PDF_URI") - val pdfTitle = intent.getStringExtra("PDF_TITLE") ?: "무제" - viewModel.documentId = intent.getLongExtra("DOCUMENT_ID", -1) + initializeView() + } - binding.titleBar.titleBar.text = pdfTitle - // 뒤로가기 버튼 클릭 리스너 설정 - binding.titleBar.backButton.setOnClickListener { - finish() - } + // 뷰 초기화 메서드 + private fun initializeView() { + viewModel.pdfUri = intent.getStringExtra(PDF_URI_KEY).toString() + viewModel.pdfTitle = intent.getStringExtra(PDF_TITLE_KEY) ?: DEFAULT_TITLE + viewModel.documentId = intent.getLongExtra(DOCUMENT_ID_KEY, -1) - // PDF URI가 있는 경우 프래그먼트 추가 - if (pdfUriString != null) { - val pdfUri = Uri.parse(pdfUriString) - val pdfViewerFragment = PdfViewerFragment.newInstance(pdfUri) - - supportFragmentManager.beginTransaction() - .replace(R.id.pdf_fragment_container, pdfViewerFragment) - .commit() - - val sideBarFragment = SideBarFragment.newInstance(viewModel.documentId, viewModel.pageNumber.value ?: 0) - supportFragmentManager.beginTransaction() - .replace(R.id.side_bar_container, sideBarFragment) - .commit() - - // 툴바의 텍스트 버튼 클릭 리스너 설정 - binding.toolbar.btnText.setOnClickListener { - pdfViewerFragment.getCurrentPdfPageFragment()?.addTextBox() - } - } else { - Log.e("NotetakingActivity", "PDF URI is null in Activity") // URI가 null인 경우 로그 - } + setupTitleBar() + setupToolbar() + setupPdfViewerAndSidebar() } + // 페이지 변경 시 호출되는 메서드 fun onPageChanged(pageNumber: Int) { viewModel.setPageNumber(pageNumber) - // 사이드바 프래그먼트 해당 페이지 내용으로 업데이트 - val sideBarFragment = supportFragmentManager.findFragmentById(R.id.side_bar_container) as? SideBarFragment + updateSidebarWithPage(pageNumber) + } + + // 툴바 설정 + private fun setupToolbar() { + binding.toolbar.btnText.setOnClickListener { + val pdfViewerFragment = getPdfViewerFragment() + pdfViewerFragment?.getCurrentPdfPageFragment()?.addTextBox() + } + } + + // 타이틀바 설정 + private fun setupTitleBar() { + binding.titleBar.backButton.setOnClickListener { + finish() + } + binding.titleBar.titleBar.text = viewModel.pdfTitle + } + + // PDF 및 사이드바 초기화 메서드 + private fun setupPdfViewerAndSidebar() { + val pdfUri = Uri.parse(viewModel.pdfUri) + replaceFragment(R.id.pdf_fragment_container, PdfViewerFragment.newInstance(pdfUri)) + replaceFragment( + R.id.side_bar_container, + SideBarFragment.newInstance(viewModel.documentId, viewModel.pageNumber.value ?: 0) + ) + } + + // 프래그먼트 교체 메서드 + private fun replaceFragment(containerId: Int, fragment: androidx.fragment.app.Fragment) { + supportFragmentManager.beginTransaction() + .replace(containerId, fragment) + .commit() + } + + // PDF 에러 처리 메서드 + private fun handlePdfError(message: String) { + Log.e("NotetakingActivity", message) + Toast.makeText(this, "PDF 파일을 열 수 없습니다.", Toast.LENGTH_SHORT).show() + } + + // PDF 뷰어 프래그먼트 가져오기 메서드 + private fun getPdfViewerFragment(): PdfViewerFragment? { + return supportFragmentManager.findFragmentById(R.id.pdf_fragment_container) as? PdfViewerFragment + } + + // 사이드바 프래그먼트 가져오기 메서드 + private fun getSideBarFragment(): SideBarFragment? { + return supportFragmentManager.findFragmentById(R.id.side_bar_container) as? SideBarFragment + } + + // 사이드바 업데이트 메서드 + private fun updateSidebarWithPage(pageNumber: Int) { + val sideBarFragment = getSideBarFragment() sideBarFragment?.updatePageNumber(pageNumber) } -} \ No newline at end of file +} diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt index f7db99b..6fc08f9 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt @@ -4,20 +4,28 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import com.iguana.notetaking.NotetakingActivity.Companion.DEFAULT_TITLE import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class NotetakingViewModel @Inject constructor( - handle: SavedStateHandle -) : ViewModel() { - private val _isSideBarVisible = MutableLiveData(true) // 초기값은 true로 설정 - val isSideBarVisible: LiveData get() = _isSideBarVisible +class NotetakingViewModel @Inject constructor() : ViewModel() { var documentId: Long = -1L + set(value) = run { field = value } + var pdfUri: String = "" + set(value) = run { field = value } + + var pdfTitle: String? = "무제" + set(value) = run { field = value } + + private val _pageNumber = MutableLiveData() val pageNumber: LiveData get() = _pageNumber + private val _isSideBarVisible = MutableLiveData(true) // 초기값은 true로 설정 + val isSideBarVisible: LiveData get() = _isSideBarVisible + fun setPageNumber(pageNumber: Int) { _pageNumber.value = pageNumber } diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt index 6632d9d..75b18ec 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt @@ -50,6 +50,7 @@ class RecordFragment() : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + } diff --git a/feature/notetaking/src/main/res/layout-sw600dp/titlebar.xml b/feature/notetaking/src/main/res/layout-sw600dp/titlebar.xml index 179df81..4a2f9df 100644 --- a/feature/notetaking/src/main/res/layout-sw600dp/titlebar.xml +++ b/feature/notetaking/src/main/res/layout-sw600dp/titlebar.xml @@ -1,6 +1,5 @@ - - + \ No newline at end of file diff --git a/feature/notetaking/src/main/res/layout/activity_notetaking.xml b/feature/notetaking/src/main/res/layout/activity_notetaking.xml index 582fc98..c32d276 100644 --- a/feature/notetaking/src/main/res/layout/activity_notetaking.xml +++ b/feature/notetaking/src/main/res/layout/activity_notetaking.xml @@ -3,9 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> - - diff --git a/feature/notetaking/src/main/res/layout/titlebar.xml b/feature/notetaking/src/main/res/layout/titlebar.xml index 6578455..28e5f65 100644 --- a/feature/notetaking/src/main/res/layout/titlebar.xml +++ b/feature/notetaking/src/main/res/layout/titlebar.xml @@ -1,6 +1,7 @@ + - - diff --git a/feature/notetaking/src/main/res/values/strings.xml b/feature/notetaking/src/main/res/values/strings.xml index b28c835..8164996 100644 --- a/feature/notetaking/src/main/res/values/strings.xml +++ b/feature/notetaking/src/main/res/values/strings.xml @@ -11,4 +11,5 @@ AI 요약이 실패했습니다. 상태를 확인할 수 없습니다. 요약 데이터가 없습니다. + 무제 \ No newline at end of file From 7a74847512a0a64a357fdf034a8fe1d32d9e4f52 Mon Sep 17 00:00:00 2001 From: aengzu Date: Thu, 24 Oct 2024 16:13:34 +0900 Subject: [PATCH 02/19] =?UTF-8?q?Feat:=20=EB=85=B9=EC=9D=8C=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=ED=86=A0=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20(=EB=85=B9=EC=9D=8C?= =?UTF-8?q?=20=EC=8B=9C=EC=9E=91,=20=EB=85=B9=EC=9D=8C=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C)=20=EB=9D=84=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iguana/notetaking/NotetakingActivity.kt | 37 +++++++++++++++--- .../iguana/notetaking/NotetakingViewModel.kt | 39 +++++++++++++++++++ 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt index 52ef38f..5b679da 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt @@ -35,6 +35,7 @@ class NotetakingActivity : AppCompatActivity() { binding.lifecycleOwner = this initializeView() + observeToolbar() } // 뷰 초기화 메서드 @@ -60,6 +61,12 @@ class NotetakingActivity : AppCompatActivity() { val pdfViewerFragment = getPdfViewerFragment() pdfViewerFragment?.getCurrentPdfPageFragment()?.addTextBox() } + binding.toolbar.btnRecord.setOnClickListener { + viewModel.toggleRecordTabActive() + } + binding.toolbar.btnAI.setOnClickListener { + viewModel.toggleAITabActive() + } } // 타이틀바 설정 @@ -72,12 +79,12 @@ class NotetakingActivity : AppCompatActivity() { // PDF 및 사이드바 초기화 메서드 private fun setupPdfViewerAndSidebar() { - val pdfUri = Uri.parse(viewModel.pdfUri) - replaceFragment(R.id.pdf_fragment_container, PdfViewerFragment.newInstance(pdfUri)) - replaceFragment( - R.id.side_bar_container, - SideBarFragment.newInstance(viewModel.documentId, viewModel.pageNumber.value ?: 0) - ) + val pdfUri = Uri.parse(viewModel.pdfUri) + replaceFragment(R.id.pdf_fragment_container, PdfViewerFragment.newInstance(pdfUri)) + replaceFragment( + R.id.side_bar_container, + SideBarFragment.newInstance(viewModel.documentId, viewModel.pageNumber.value ?: 0) + ) } // 프래그먼트 교체 메서드 @@ -108,4 +115,22 @@ class NotetakingActivity : AppCompatActivity() { val sideBarFragment = getSideBarFragment() sideBarFragment?.updatePageNumber(pageNumber) } + + // 뷰모델의 상태를 관찰하여 UI 업데이트 + private fun observeToolbar() { + // Record 탭의 활성화 상태 관찰 + viewModel.isRecordActive.observe(this) { isActive -> + binding.toolbar.btnRecord.isSelected = isActive // 선택된 상태로 업데이트 + if (isActive) { + Toast.makeText(this, "녹음이 시작되었습니다.", Toast.LENGTH_SHORT).show() + } else if (viewModel.isRecordingStopped()) { + Toast.makeText(this, "녹음이 종료되었습니다.", Toast.LENGTH_SHORT).show() + } + } + + // AI 탭의 활성화 상태 관찰 + viewModel.isAIActive.observe(this) { isActive -> + binding.toolbar.btnAI.isSelected = isActive // 선택된 상태로 업데이트 + } + } } diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt index 6fc08f9..2b4a8e4 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt @@ -26,6 +26,18 @@ class NotetakingViewModel @Inject constructor() : ViewModel() { private val _isSideBarVisible = MutableLiveData(true) // 초기값은 true로 설정 val isSideBarVisible: LiveData get() = _isSideBarVisible + // record 버튼 활성화되어있는지 + private val _isRecordActive = MutableLiveData(false) + val isRecordActive: LiveData get() = _isRecordActive + + + // AI 버튼 활성화되어있는지 + private val _isAIActive = MutableLiveData(false) + val isAIActive: LiveData get() = _isAIActive + + // 이전 녹음 상태 추적 변수 + private var wasRecording = false + fun setPageNumber(pageNumber: Int) { _pageNumber.value = pageNumber } @@ -34,4 +46,31 @@ class NotetakingViewModel @Inject constructor() : ViewModel() { fun toggleSideBarVisibility() { _isSideBarVisible.value = _isSideBarVisible.value?.not() } + + // 사이드바 보이게 하는 함수 + fun showSideBar() { + _isSideBarVisible.value = true + } + // 사이드바 숨기기 + fun hideSideBar() { + _isSideBarVisible.value = false + } + + + // 액티브 상태를 토글 + fun toggleRecordTabActive() { + _isRecordActive.value = _isRecordActive.value?.not() + // 상태가 변경될 때 이전 녹음 상태를 저장 + wasRecording = _isRecordActive.value == true + showSideBar() + } + fun toggleAITabActive() { + _isAIActive.value = _isAIActive.value?.not() + showSideBar() + } + + // 녹음이 종료되었는지 확인하는 함수 (이전에 녹음 중 -> 녹음 종료) + fun isRecordingStopped(): Boolean { + return wasRecording && !_isRecordActive.value!! + } } From 8941e6a8a78e6171dc5975d37e703e686c029937 Mon Sep 17 00:00:00 2001 From: aengzu Date: Thu, 24 Oct 2024 18:14:39 +0900 Subject: [PATCH 03/19] =?UTF-8?q?Chore:=20Manifest=20=EC=97=90=20Record=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/notetaking/src/main/AndroidManifest.xml | 11 +---------- .../com/iguana/notetaking/recording/RecordFragment.kt | 4 ++++ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/feature/notetaking/src/main/AndroidManifest.xml b/feature/notetaking/src/main/AndroidManifest.xml index 730dd70..61b5ec6 100644 --- a/feature/notetaking/src/main/AndroidManifest.xml +++ b/feature/notetaking/src/main/AndroidManifest.xml @@ -5,15 +5,6 @@ - - - - + \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt index 75b18ec..376be7a 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt @@ -6,9 +6,11 @@ import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.SavedStateViewModelFactory import com.iguana.notetaking.NotetakingActivity +import com.iguana.notetaking.NotetakingViewModel import com.iguana.notetaking.ai.AiFragment import com.iguana.notetaking.databinding.FragmentRecordBinding import dagger.hilt.android.AndroidEntryPoint @@ -29,6 +31,8 @@ class RecordFragment() : Fragment() { private var _binding: FragmentRecordBinding? = null private val binding get() = _binding!! + + private val notetakingViewModel: NotetakingViewModel by activityViewModels() private val viewModel: RecordViewModel by viewModels() override fun onCreateView( From 83cb7df3ace15c9467ca20729354f65a1018a4b0 Mon Sep 17 00:00:00 2001 From: aengzu Date: Thu, 24 Oct 2024 22:06:34 +0900 Subject: [PATCH 04/19] =?UTF-8?q?Feat:=20=EB=85=B9=EC=9D=8C=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EB=85=B9=EC=9D=8C?= =?UTF-8?q?=20=EC=8B=9C=EC=9E=91=20=EB=B0=8F=20=EC=A2=85=EB=A3=8C(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *NotetakingActivity의 툴바에서 녹음 버튼 클릭 -> 녹음 상태 활성화 -> SidebarFragment 로 녹음 시작 이벤트 전달 -> SidebarFragment 에서 하위 탭인 RecordFragment 로 녹음 시작 이벤트 전달 -> RecordViewModel 의 startRecording() 호출 -> RecordingService 에서 MediaRecorder 사용해서 앱 내부 저장소에 녹음 파일 저장 *실제로 녹음을 앱 내에서 재생을 할 지 서버에 전송만 하고 녹음 파일을 삭제할 지는 해봐야 알 것 같습니다. --- .../notetaking/src/main/AndroidManifest.xml | 9 + .../iguana/notetaking/NotetakingActivity.kt | 43 +++++ .../notetaking/recording/RecordFragment.kt | 28 +++- .../notetaking/recording/RecordViewModel.kt | 33 ++++ .../notetaking/recording/RecordingService.kt | 112 +++++++++++++ .../notetaking/sidebar/SideBarFragment.kt | 156 ++++++++++-------- .../src/main/res/layout/fragment_record.xml | 84 +++++++--- .../src/main/res/values/strings.xml | 2 + 8 files changed, 366 insertions(+), 101 deletions(-) create mode 100644 feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt diff --git a/feature/notetaking/src/main/AndroidManifest.xml b/feature/notetaking/src/main/AndroidManifest.xml index 61b5ec6..a44089f 100644 --- a/feature/notetaking/src/main/AndroidManifest.xml +++ b/feature/notetaking/src/main/AndroidManifest.xml @@ -5,6 +5,15 @@ + + + + + \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt index 5b679da..d96748a 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt @@ -1,17 +1,22 @@ package com.iguana.notetaking +import android.Manifest +import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle import android.util.Log +import android.view.WindowInsets.Side import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.iguana.notetaking.databinding.ActivityNotetakingBinding +import com.iguana.notetaking.recording.RecordFragment import com.iguana.notetaking.sidebar.SideBarFragment import dagger.hilt.android.AndroidEntryPoint + @AndroidEntryPoint class NotetakingActivity : AppCompatActivity() { @@ -62,6 +67,7 @@ class NotetakingActivity : AppCompatActivity() { pdfViewerFragment?.getCurrentPdfPageFragment()?.addTextBox() } binding.toolbar.btnRecord.setOnClickListener { + requestAudioPermissions() viewModel.toggleRecordTabActive() } binding.toolbar.btnAI.setOnClickListener { @@ -122,8 +128,10 @@ class NotetakingActivity : AppCompatActivity() { viewModel.isRecordActive.observe(this) { isActive -> binding.toolbar.btnRecord.isSelected = isActive // 선택된 상태로 업데이트 if (isActive) { + startRecordingInFragment() Toast.makeText(this, "녹음이 시작되었습니다.", Toast.LENGTH_SHORT).show() } else if (viewModel.isRecordingStopped()) { + stopRecordingInFragment() Toast.makeText(this, "녹음이 종료되었습니다.", Toast.LENGTH_SHORT).show() } } @@ -133,4 +141,39 @@ class NotetakingActivity : AppCompatActivity() { binding.toolbar.btnAI.isSelected = isActive // 선택된 상태로 업데이트 } } + + private val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 1001 + + // 권한 요청 메서드 + private fun requestAudioPermissions() { + if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), RECORD_AUDIO_PERMISSION_REQUEST_CODE) + } else { + // 이미 권한이 있는 경우 + Log.d("NotetakingActivity", "이미 녹음 권한이 있습니다.") + } + } + + // 권한 요청 결과 처리 + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + + } else { + Toast.makeText(this, "녹음 권한이 필요합니다.", Toast.LENGTH_SHORT).show() + } + } + } + // 녹음 시작 메서드 + private fun startRecordingInFragment() { + val sideBarFragment = getSideBarFragment() // SidebarFragment 가져오기 + sideBarFragment?.startRecordingInRecordFragment() // SidebarFragment에 녹음 시작 요청 + } + + // 녹음 중지 메서드 + private fun stopRecordingInFragment() { + val sideBarFragment = getSideBarFragment() // SidebarFragment 가져오기 + sideBarFragment?.stopRecordingInRecordFragment() // SidebarFragment에 녹음 중지 요청 + } } diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt index 376be7a..61ac47d 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt @@ -1,6 +1,8 @@ package com.iguana.notetaking.recording +import android.content.Context import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -31,8 +33,6 @@ class RecordFragment() : Fragment() { private var _binding: FragmentRecordBinding? = null private val binding get() = _binding!! - - private val notetakingViewModel: NotetakingViewModel by activityViewModels() private val viewModel: RecordViewModel by viewModels() override fun onCreateView( @@ -54,7 +54,7 @@ class RecordFragment() : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - + observeRecordingState() } @@ -70,4 +70,26 @@ class RecordFragment() : Fragment() { _binding = null } + fun startRecording(context: Context) { + Log.d("RecordFragment", "녹음이 시작되기 바로 직전입니다.") + viewModel.startRecording(context) + Log.d("RecordFragment", "녹음이 시작되었습니다.") + } + + fun stopRecording(context: Context) { + viewModel.stopRecording(context) + } + + private fun observeRecordingState() { + viewModel.recordingStatus.observe(viewLifecycleOwner) { isRecording -> + if (isRecording) { + binding.recordStatusTextView.text = "녹음 중" + } else { + binding.recordStatusTextView.text = "녹음 종료" + } + } + } + + + } diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt index 92c577c..4204534 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt @@ -1,5 +1,10 @@ package com.iguana.notetaking.recording +import android.content.Context +import android.content.Intent +import android.media.MediaRecorder +import android.util.Log +import androidx.core.content.PackageManagerCompat.LOG_TAG import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle @@ -7,6 +12,7 @@ import androidx.lifecycle.ViewModel import com.iguana.notetaking.ai.AiFragment import com.iguana.notetaking.sidebar.SideBarFragment import dagger.hilt.android.lifecycle.HiltViewModel +import java.io.IOException import javax.inject.Inject @HiltViewModel @@ -14,6 +20,11 @@ class RecordViewModel @Inject constructor( handle: SavedStateHandle ) : ViewModel() { var documentId: Long = -1L + private var recorder: MediaRecorder? = null + private var isRecording = false // 녹음 상태 확인 변수 + + private val _recordingStatus = MutableLiveData() + val recordingStatus: LiveData get() = _recordingStatus private val _pageNumber = MutableLiveData() val pageNumber: LiveData get() = _pageNumber @@ -21,4 +32,26 @@ class RecordViewModel @Inject constructor( fun setPageNumber(pageNumber: Int) { _pageNumber.value = pageNumber } + + fun startRecording(context: Context) { + try { + val intent = Intent(context, RecordingService::class.java).apply { + action = "START_RECORDING" + } + context.startService(intent) + _recordingStatus.value = true + } catch (e: Exception) { + Log.e("RecordFragment", "녹음 시작 실패: ${e.message}") + } + } + + fun stopRecording(context: Context) { + Log.d("RecordViewModel", "녹음이 중지되기 바로 직전입니다.(뷰모델)") + val intent = Intent(context, RecordingService::class.java).apply { + action = "STOP_RECORDING" + } + context.startService(intent) + Log.d("RecordViewModel", "녹음이 중지되었습니다.(뷰모델)") + _recordingStatus.value = false + } } \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt new file mode 100644 index 0000000..1e8fb0b --- /dev/null +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt @@ -0,0 +1,112 @@ +package com.iguana.notetaking.recording + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.media.MediaRecorder +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import java.io.IOException + +class RecordingService : Service() { + + private var recorder: MediaRecorder? = null + private var isRecording = false + private val channelId = "RecordingServiceChannel" + private val notificationId = 1 + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val action = intent?.action + Log.d("RecordingService", "onStartCommand: $action") + + when (action) { + "START_RECORDING" -> startRecording() + "STOP_RECORDING" -> stopRecording() + } + + return START_STICKY + } + + private fun startRecording() { + Log.d("RecordingService", "녹음이 시작되기 바로 직전입니다.") + if (isRecording) { + Log.w("RecordingService", "녹음이 이미 진행 중입니다.") + return + } + // 파일 경로 생성 + val outputFilePath = getRecordingFilePath(this) + + recorder = MediaRecorder().apply { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) + setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) + setOutputFile(outputFilePath) + + try { + prepare() + start() + isRecording = true + startForeground(notificationId, createRecordingNotification("녹음 중...")) + Log.d("RecordingService", "녹음이 시작되었습니다.") + } catch (e: IOException) { + Log.e("RecordingService", "prepare() failed: ${e.message}") + } + } + } + + private fun stopRecording() { + if (!isRecording) { + Log.w("RecordingService", "현재 녹음이 진행 중이지 않습니다.") + return + } + + recorder?.apply { + stop() + release() + } + recorder = null + isRecording = false + stopForeground(true) + stopSelf() + Log.d("RecordingService", "녹음이 종료되었습니다.") + } + + private fun createRecordingNotification(contentText: String): Notification { + return NotificationCompat.Builder(this, channelId) + .setContentTitle("녹음 서비스") + .setContentText(contentText) + .setSmallIcon(com.iguana.designsystem.R.drawable.ic_record_active) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + } + + private fun createNotificationChannel() { + val channel = NotificationChannel( + channelId, + "녹음 서비스 채널", + NotificationManager.IMPORTANCE_LOW + ) + + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + fun getRecordingFilePath(context: Context): String { + // 외부 저장소의 앱 전용 디렉토리 + val directory = context.getExternalFilesDir(null) + return "${directory?.absolutePath}/recording_${System.currentTimeMillis()}.3gp" + } +} diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/sidebar/SideBarFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/sidebar/SideBarFragment.kt index 5939323..b04d3a9 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/sidebar/SideBarFragment.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/sidebar/SideBarFragment.kt @@ -1,86 +1,100 @@ -package com.iguana.notetaking.sidebar - -import androidx.fragment.app.viewModels -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.os.bundleOf -import com.google.android.material.tabs.TabLayoutMediator -import com.iguana.notetaking.ai.AiFragment -import com.iguana.notetaking.databinding.FragmentSideBarBinding -import com.iguana.notetaking.recording.RecordFragment -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class SideBarFragment() : Fragment() { - - companion object { - const val DOCUMENT_ID = "documentId" - const val CURRENT_PAGE = "currentPage" - fun newInstance(documentId: Long, currentPage: Int) = SideBarFragment().apply { - arguments = bundleOf( - DOCUMENT_ID to documentId, - CURRENT_PAGE to currentPage - ) + package com.iguana.notetaking.sidebar + + import androidx.fragment.app.viewModels + import android.os.Bundle + import androidx.fragment.app.Fragment + import android.view.LayoutInflater + import android.view.View + import android.view.ViewGroup + import androidx.core.os.bundleOf + import com.google.android.material.tabs.TabLayoutMediator + import com.iguana.notetaking.R + import com.iguana.notetaking.ai.AiFragment + import com.iguana.notetaking.databinding.FragmentSideBarBinding + import com.iguana.notetaking.recording.RecordFragment + import dagger.hilt.android.AndroidEntryPoint + + @AndroidEntryPoint + class SideBarFragment() : Fragment() { + + companion object { + const val DOCUMENT_ID = "documentId" + const val CURRENT_PAGE = "currentPage" + fun newInstance(documentId: Long, currentPage: Int) = SideBarFragment().apply { + arguments = bundleOf( + DOCUMENT_ID to documentId, + CURRENT_PAGE to currentPage + ) + } } - } - private val viewModel: SideBarViewModel by viewModels() - private var _binding: FragmentSideBarBinding? = null - private val binding get() = _binding!! + private val viewModel: SideBarViewModel by viewModels() + private var _binding: FragmentSideBarBinding? = null + private val binding get() = _binding!! - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentSideBarBinding.inflate(inflater, container, false) + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSideBarBinding.inflate(inflater, container, false) - arguments?.let { - viewModel.documentId = it.getLong(DOCUMENT_ID) - viewModel.setPageNumber(it.getInt(CURRENT_PAGE)) + arguments?.let { + viewModel.documentId = it.getLong(DOCUMENT_ID) + viewModel.setPageNumber(it.getInt(CURRENT_PAGE)) + } + + return binding.root } - return binding.root - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + val viewPager = binding.sideBarViewPager + val tabLayout = binding.sideBarTabLayout - val viewPager = binding.sideBarViewPager - val tabLayout = binding.sideBarTabLayout + viewPager.adapter = SidebarAdapter(this, viewModel.documentId, viewModel.pageNumber.value ?: 0) - viewPager.adapter = SidebarAdapter(this, viewModel.documentId, viewModel.pageNumber.value ?: 0) + // 탭 레이아웃과 뷰페이저 연결 + TabLayoutMediator(tabLayout, viewPager) { tab, position -> + tab.text = when (position) { + 0 -> "녹음" + 1 -> "AI" + else -> throw IllegalArgumentException("Invalid position") + } + }.attach() - // 탭 레이아웃과 뷰페이저 연결 - TabLayoutMediator(tabLayout, viewPager) { tab, position -> - tab.text = when (position) { - 0 -> "녹음" - 1 -> "AI" - else -> throw IllegalArgumentException("Invalid position") + // 페이지 번호 변경 시 호출되는 메서드 + viewModel.pageNumber.observe(viewLifecycleOwner) { pageNumber -> + (viewPager.adapter as SidebarAdapter).getFragment(0)?.let { + (it as RecordFragment).updateContentForPage(pageNumber) + } + (viewPager.adapter as SidebarAdapter).getFragment(1)?.let { + (it as AiFragment).updateContentForPage(pageNumber) + } } - }.attach() - - // 페이지 번호 변경 시 호출되는 메서드 - viewModel.pageNumber.observe(viewLifecycleOwner) { pageNumber -> - (viewPager.adapter as SidebarAdapter).getFragment(0)?.let { - (it as RecordFragment).updateContentForPage(pageNumber) + } + // RecordFragment에서 녹음 시작 + fun startRecordingInRecordFragment() { + (binding.sideBarViewPager.adapter as SidebarAdapter).getFragment(0)?.let { + (it as RecordFragment).startRecording(requireContext()) } - (viewPager.adapter as SidebarAdapter).getFragment(1)?.let { - (it as AiFragment).updateContentForPage(pageNumber) + } + + // RecordFragment에서 녹음 중지 + fun stopRecordingInRecordFragment() { + (binding.sideBarViewPager.adapter as SidebarAdapter).getFragment(0)?.let { + (it as RecordFragment).stopRecording(requireContext()) } } - } - - // 사이드바 내용 업데이트 메서드 - fun updatePageNumber(pageNumber: Int) { - viewModel.setPageNumber(pageNumber) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file + + // 사이드바 내용 업데이트 메서드 + fun updatePageNumber(pageNumber: Int) { + viewModel.setPageNumber(pageNumber) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + } \ No newline at end of file diff --git a/feature/notetaking/src/main/res/layout/fragment_record.xml b/feature/notetaking/src/main/res/layout/fragment_record.xml index 799c21d..5e501ed 100644 --- a/feature/notetaking/src/main/res/layout/fragment_record.xml +++ b/feature/notetaking/src/main/res/layout/fragment_record.xml @@ -1,11 +1,14 @@ + + + @@ -15,40 +18,67 @@ android:id="@+id/boxView" android:layout_width="180dp" android:layout_height="36dp" - android:padding="8dp" + android:layout_marginTop="20dp" android:background="@drawable/page_box_background" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintStart_toStartOf="parent" + android:padding="8dp" app:layout_constraintEnd_toEndOf="parent" - android:layout_marginTop="20dp"> - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + + app:layout_constraintTop_toBottomOf="@id/boxView" /> + + + + + + + + + + \ No newline at end of file diff --git a/feature/notetaking/src/main/res/values/strings.xml b/feature/notetaking/src/main/res/values/strings.xml index 8164996..401fb6e 100644 --- a/feature/notetaking/src/main/res/values/strings.xml +++ b/feature/notetaking/src/main/res/values/strings.xml @@ -12,4 +12,6 @@ 상태를 확인할 수 없습니다. 요약 데이터가 없습니다. 무제 + 녹음 중이 아닙니다. + 녹음 중.. \ No newline at end of file From ec4bbdd09b413c083075a81470c9ef22b15dec47 Mon Sep 17 00:00:00 2001 From: aengzu Date: Fri, 25 Oct 2024 16:40:05 +0900 Subject: [PATCH 05/19] =?UTF-8?q?Feat:=20=EC=84=9C=EB=B2=84=EC=97=90=20?= =?UTF-8?q?=EB=85=B9=EC=9D=8C=20=ED=8C=8C=EC=9D=BC=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?Usecase=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/iguana/data/remote/model/RecordDto.kt | 1 - .../iguana/data/repository/RecordRepositoryImpl.kt | 2 +- .../iguana/domain/usecase/UploadRecordingUseCase.kt | 13 +++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt diff --git a/core/data/src/main/java/com/iguana/data/remote/model/RecordDto.kt b/core/data/src/main/java/com/iguana/data/remote/model/RecordDto.kt index 3d11e9f..332ddfa 100644 --- a/core/data/src/main/java/com/iguana/data/remote/model/RecordDto.kt +++ b/core/data/src/main/java/com/iguana/data/remote/model/RecordDto.kt @@ -19,7 +19,6 @@ data class RecordingUploadRequestDto( data class RecordingUploadResponseDto( val recordingId: Long, val documentId: Long, - val url: String, val createdAt: String // ISO 8601 format ) diff --git a/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt b/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt index 08c8ad5..d75a802 100644 --- a/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt +++ b/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt @@ -24,7 +24,7 @@ class RecordRepositoryImpl @Inject constructor( override suspend fun uploadRecordingFile(recordingFile: RecordingFile): RecordingFile { return withContext(Dispatchers.IO) { val uploadRequest = recordingFile.toUploadRequestDto() - val response = recordApi.uploadRecording(recordingFile.documentName.toLong(), uploadRequest) + val response = recordApi.uploadRecording(recordingFile.documentId ?: throw AppError.NullResponseError("Document ID가 없습니다."), uploadRequest) if (response.isSuccessful) { val body = response.body() ?: throw AppError.NullResponseError("녹음 파일 업로드 응답이 비어 있습니다.") diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt new file mode 100644 index 0000000..2f21abd --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt @@ -0,0 +1,13 @@ +package com.iguana.domain.usecase + +import com.iguana.domain.model.record.RecordingFile +import com.iguana.domain.repository.RecordRepository +import javax.inject.Inject + +class UploadRecordingUseCase @Inject constructor( + private val recordRepository: RecordRepository +) { + suspend operator fun invoke(documentId: Long, recordingFile: RecordingFile): RecordingFile { + return recordRepository.uploadRecordingFile(recordingFile) + } +} \ No newline at end of file From d5bbb56f64ef1da2cd08b93ed8dee5ef4eb9b3b8 Mon Sep 17 00:00:00 2001 From: aengzu Date: Fri, 25 Oct 2024 16:43:24 +0900 Subject: [PATCH 06/19] =?UTF-8?q?Feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=9C=A0?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/usecase/UploadPageTurnEventsUseCase.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt new file mode 100644 index 0000000..2a78a0b --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt @@ -0,0 +1,13 @@ +package com.iguana.domain.usecase + +import com.iguana.domain.model.record.PageTurnEvent +import com.iguana.domain.repository.RecordRepository +import javax.inject.Inject + +class UploadPageTurnEventsUseCase @Inject constructor( + private val recordRepository: RecordRepository +) { + suspend operator fun invoke(documentId: Long, recordingId: Long, events: List) { + recordRepository.uploadPageTurnEvents(recordingId, events) + } +} \ No newline at end of file From 37dc8dc74c25fa3ee94840898e83fb5fde9ade7c Mon Sep 17 00:00:00 2001 From: aengzu Date: Fri, 25 Oct 2024 21:15:12 +0900 Subject: [PATCH 07/19] =?UTF-8?q?Feat:=20Page=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20Room=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/data/build.gradle.kts | 5 ++-- .../java/com/iguana/data/di/DataModule.kt | 25 ++++++++++++++++++ .../java/com/iguana/data/di/NetworkModule.kt | 7 +++++ .../com/iguana/data/di/RepositoryModule.kt | 9 +++++++ .../com/iguana/data/local/dao/RecordingDao.kt | 22 ++++++++++++++++ .../iguana/data/local/db/RecordingDatabase.kt | 11 ++++++++ .../data/local/entity/PageTurnEventEntity.kt | 13 ++++++++++ .../data/local/files/RecordingFileStorage.kt | 2 +- .../domain/usecase/UploadRecordingUseCase.kt | 26 ++++++++++++++++++- .../notetaking/recording/RecordViewModel.kt | 14 +++++----- 10 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 core/data/src/main/java/com/iguana/data/local/dao/RecordingDao.kt create mode 100644 core/data/src/main/java/com/iguana/data/local/db/RecordingDatabase.kt create mode 100644 core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 6e352f3..6ddd087 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -25,11 +25,10 @@ dependencies { implementation(libs.retrofit.core) implementation(libs.retrofit.kotlin.serialization) implementation(libs.retrofit.gson) - implementation(libs.room.ktx) - kapt(libs.room.compiler) implementation(libs.room.runtime) + implementation(libs.room.ktx) implementation(libs.coroutines.android) - implementation(projects.core.domain) // domain 에 있는 걸 구현하므로 + implementation(projects.core.domain) implementation(libs.okhttp.logging) } diff --git a/core/data/src/main/java/com/iguana/data/di/DataModule.kt b/core/data/src/main/java/com/iguana/data/di/DataModule.kt index b4477cc..3734148 100644 --- a/core/data/src/main/java/com/iguana/data/di/DataModule.kt +++ b/core/data/src/main/java/com/iguana/data/di/DataModule.kt @@ -3,7 +3,9 @@ package com.iguana.data.di import android.content.Context import androidx.room.Room import com.iguana.data.local.dao.RecentFileDao +import com.iguana.data.local.dao.RecordingDao import com.iguana.data.local.db.AppDatabase +import com.iguana.data.local.db.RecordingDatabase import com.iguana.domain.repository.SharedPreferencesHelper import com.iguana.data.local.db.SharedPreferencesHelperImpl import com.iguana.data.local.files.FileHelperImpl @@ -14,6 +16,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import java.io.File import javax.inject.Singleton @Module @@ -48,5 +51,27 @@ abstract class DataModule { fun provideRecentFileDao(appDatabase: AppDatabase): RecentFileDao { return appDatabase.recentFileDao() } + + @Provides + @Singleton + fun provideRecordingDatabase(@ApplicationContext context: Context): RecordingDatabase { + return Room.databaseBuilder( + context, + RecordingDatabase::class.java, + "recording_database" + ).build() + } + + @Provides + fun provideRecordingDao(recordingDatabase: RecordingDatabase): RecordingDao { + return recordingDatabase.recordingDao() + } + + @Provides + @Singleton + fun provideBaseDir(@ApplicationContext context: Context): File { + // 앱의 내부 파일 디렉토리를 기본 디렉토리로 사용 + return context.filesDir + } } } \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/di/NetworkModule.kt b/core/data/src/main/java/com/iguana/data/di/NetworkModule.kt index 988bc06..5aae0b5 100644 --- a/core/data/src/main/java/com/iguana/data/di/NetworkModule.kt +++ b/core/data/src/main/java/com/iguana/data/di/NetworkModule.kt @@ -3,6 +3,7 @@ package com.iguana.data.di import com.iguana.data.BuildConfig import com.iguana.data.remote.api.AnnotationApi import com.iguana.data.remote.api.DocumentApi +import com.iguana.data.remote.api.RecordApi import com.iguana.data.remote.api.SummarizeApi import com.iguana.domain.repository.SharedPreferencesHelper import dagger.Module @@ -59,4 +60,10 @@ object NetworkModule { fun provideSummarizeApi(retrofit: Retrofit): SummarizeApi { return retrofit.create(SummarizeApi::class.java) } + + @Provides + @Singleton + fun provideRecordApi(retrofit: Retrofit): RecordApi { + return retrofit.create(RecordApi::class.java) + } } diff --git a/core/data/src/main/java/com/iguana/data/di/RepositoryModule.kt b/core/data/src/main/java/com/iguana/data/di/RepositoryModule.kt index 05c532e..20444ad 100644 --- a/core/data/src/main/java/com/iguana/data/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/iguana/data/di/RepositoryModule.kt @@ -5,11 +5,13 @@ import com.iguana.data.repository.AnnotationRepositoryImpl import com.iguana.data.repository.DocumentsRepositoryImpl import com.iguana.data.repository.LoginRepositoryImpl import com.iguana.data.repository.RecentFileRepositoryImpl +import com.iguana.data.repository.RecordRepositoryImpl import com.iguana.domain.repository.AIRepository import com.iguana.domain.repository.AnnotationRepository import com.iguana.domain.repository.DocumentsRepository import com.iguana.domain.repository.LoginRepository import com.iguana.domain.repository.RecentFileRepository +import com.iguana.domain.repository.RecordRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -48,4 +50,11 @@ abstract class RepositoryModule { abstract fun bindAIRepository( aiRepositoryImpl: AIRepositoryImpl ): AIRepository + + @Binds + @Singleton + abstract fun bindRecordingRepository( + recordRepositoryImpl: RecordRepositoryImpl + ): RecordRepository + } \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/local/dao/RecordingDao.kt b/core/data/src/main/java/com/iguana/data/local/dao/RecordingDao.kt new file mode 100644 index 0000000..63841db --- /dev/null +++ b/core/data/src/main/java/com/iguana/data/local/dao/RecordingDao.kt @@ -0,0 +1,22 @@ +package com.iguana.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.iguana.data.local.entity.PageTurnEventEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface RecordingDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertPageTurnEvent(event: PageTurnEventEntity) + + @Query("SELECT * FROM page_turn_events WHERE documentId = :documentId") + fun getPageTurnEvents(documentId: Long): Flow> + + @Query("DELETE FROM page_turn_events WHERE documentId = :documentId") + suspend fun deletePageTurnEvents(documentId: Long): Integer +} + diff --git a/core/data/src/main/java/com/iguana/data/local/db/RecordingDatabase.kt b/core/data/src/main/java/com/iguana/data/local/db/RecordingDatabase.kt new file mode 100644 index 0000000..68c6780 --- /dev/null +++ b/core/data/src/main/java/com/iguana/data/local/db/RecordingDatabase.kt @@ -0,0 +1,11 @@ +package com.iguana.data.local.db + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.iguana.data.local.dao.RecordingDao +import com.iguana.data.local.entity.PageTurnEventEntity + +@Database(entities = [PageTurnEventEntity::class], version = 1, exportSchema = false) +abstract class RecordingDatabase : RoomDatabase() { + abstract fun recordingDao(): RecordingDao +} \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt b/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt new file mode 100644 index 0000000..34d31f7 --- /dev/null +++ b/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt @@ -0,0 +1,13 @@ +package com.iguana.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "page_turn_events") +data class PageTurnEventEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val documentId: Long, + val prevPage: Int, + val nextPage: Int, + val timestamp: Double +) diff --git a/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt b/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt index ac88ecf..1cd6804 100644 --- a/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt +++ b/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt @@ -6,7 +6,7 @@ import java.io.File import javax.inject.Inject class RecordingFileStorage @Inject constructor( - private val baseDir: File, // 내부 저장소 경로 (context.filesDir 등을 사용할 수 있음) + private val baseDir: File ) { // 로컬 스토리지에 녹음 파일 저장 diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt index 2f21abd..1a33714 100644 --- a/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt +++ b/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt @@ -2,12 +2,36 @@ package com.iguana.domain.usecase import com.iguana.domain.model.record.RecordingFile import com.iguana.domain.repository.RecordRepository +import java.io.File import javax.inject.Inject class UploadRecordingUseCase @Inject constructor( private val recordRepository: RecordRepository ) { - suspend operator fun invoke(documentId: Long, recordingFile: RecordingFile): RecordingFile { + suspend operator fun invoke( + documentId: Long, + filePath: String, + fileName: String + ): RecordingFile { + val recordingFile = createRecordingFile(documentId, filePath, fileName) return recordRepository.uploadRecordingFile(recordingFile) } + + // RecordingFile 객체 생성 메서드 + private fun createRecordingFile( + documentId: Long, + filePath: String, + fileName: String + ): RecordingFile { + val file = File(filePath) + return RecordingFile( + filePath = filePath, + fileSize = file.length(), + format = "3gp", + duration = 0L, // 녹음 길이 설정 필요 시 추가 + documentName = fileName, + recordingId = null, + documentId = documentId + ) + } } \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt index 4204534..cb63721 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt @@ -4,20 +4,20 @@ import android.content.Context import android.content.Intent import android.media.MediaRecorder import android.util.Log -import androidx.core.content.PackageManagerCompat.LOG_TAG import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import com.iguana.notetaking.ai.AiFragment -import com.iguana.notetaking.sidebar.SideBarFragment +import com.iguana.domain.usecase.UploadPageTurnEventsUseCase +import com.iguana.domain.usecase.UploadRecordingUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import java.io.IOException import javax.inject.Inject @HiltViewModel class RecordViewModel @Inject constructor( - handle: SavedStateHandle + handle: SavedStateHandle, + private val uploadRecordingUseCase: UploadRecordingUseCase, + private val uploadPageTurnEventsUseCase: UploadPageTurnEventsUseCase ) : ViewModel() { var documentId: Long = -1L private var recorder: MediaRecorder? = null @@ -41,17 +41,15 @@ class RecordViewModel @Inject constructor( context.startService(intent) _recordingStatus.value = true } catch (e: Exception) { - Log.e("RecordFragment", "녹음 시작 실패: ${e.message}") + Log.e("RecordViewModel", "녹음 시작 실패: ${e.message}") } } fun stopRecording(context: Context) { - Log.d("RecordViewModel", "녹음이 중지되기 바로 직전입니다.(뷰모델)") val intent = Intent(context, RecordingService::class.java).apply { action = "STOP_RECORDING" } context.startService(intent) - Log.d("RecordViewModel", "녹음이 중지되었습니다.(뷰모델)") _recordingStatus.value = false } } \ No newline at end of file From d86cd7593f13cbb937ba51ac6393c340a9d47197 Mon Sep 17 00:00:00 2001 From: aengzu Date: Sun, 27 Oct 2024 01:28:13 +0900 Subject: [PATCH 08/19] =?UTF-8?q?Feat:=20=EB=85=B9=EC=9D=8C=20=EB=8F=84?= =?UTF-8?q?=EC=A4=91=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=EC=97=90=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/deploymentTargetDropDown.xml | 15 ++++++- core/data/build.gradle.kts | 1 + .../java/com/iguana/data/di/DataModule.kt | 17 -------- .../com/iguana/data/local/dao/RecordingDao.kt | 22 ---------- .../iguana/data/local/db/RecordingDatabase.kt | 11 ----- .../data/local/entity/PageTurnEventEntity.kt | 13 ------ .../data/local/files/RecordingFileStorage.kt | 14 ++++--- .../com/iguana/data/mapper/RecordMapper.kt | 15 +++++-- .../data/repository/RecordRepositoryImpl.kt | 5 ++- .../domain/model/record/PageTurnEvent.kt | 7 ++-- .../domain/repository/RecordRepository.kt | 2 +- .../usecase/SavePageTurnEventsUseCase.kt | 29 +++++++++++++ .../notetaking/src/main/AndroidManifest.xml | 1 - .../notetaking/recording/RecordFragment.kt | 2 +- .../notetaking/recording/RecordViewModel.kt | 31 ++++++++++++-- .../notetaking/recording/RecordingService.kt | 42 +++++++++++++++---- 16 files changed, 133 insertions(+), 94 deletions(-) delete mode 100644 core/data/src/main/java/com/iguana/data/local/dao/RecordingDao.kt delete mode 100644 core/data/src/main/java/com/iguana/data/local/db/RecordingDatabase.kt delete mode 100644 core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt create mode 100644 core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventsUseCase.kt diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 0c0c338..03942f2 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -3,7 +3,20 @@ - + + + + + + + + + + + + + + diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 6ddd087..73a9b08 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(libs.room.runtime) implementation(libs.room.ktx) implementation(libs.coroutines.android) + kapt(libs.room.compiler) implementation(projects.core.domain) implementation(libs.okhttp.logging) } diff --git a/core/data/src/main/java/com/iguana/data/di/DataModule.kt b/core/data/src/main/java/com/iguana/data/di/DataModule.kt index 3734148..c709338 100644 --- a/core/data/src/main/java/com/iguana/data/di/DataModule.kt +++ b/core/data/src/main/java/com/iguana/data/di/DataModule.kt @@ -3,9 +3,7 @@ package com.iguana.data.di import android.content.Context import androidx.room.Room import com.iguana.data.local.dao.RecentFileDao -import com.iguana.data.local.dao.RecordingDao import com.iguana.data.local.db.AppDatabase -import com.iguana.data.local.db.RecordingDatabase import com.iguana.domain.repository.SharedPreferencesHelper import com.iguana.data.local.db.SharedPreferencesHelperImpl import com.iguana.data.local.files.FileHelperImpl @@ -52,21 +50,6 @@ abstract class DataModule { return appDatabase.recentFileDao() } - @Provides - @Singleton - fun provideRecordingDatabase(@ApplicationContext context: Context): RecordingDatabase { - return Room.databaseBuilder( - context, - RecordingDatabase::class.java, - "recording_database" - ).build() - } - - @Provides - fun provideRecordingDao(recordingDatabase: RecordingDatabase): RecordingDao { - return recordingDatabase.recordingDao() - } - @Provides @Singleton fun provideBaseDir(@ApplicationContext context: Context): File { diff --git a/core/data/src/main/java/com/iguana/data/local/dao/RecordingDao.kt b/core/data/src/main/java/com/iguana/data/local/dao/RecordingDao.kt deleted file mode 100644 index 63841db..0000000 --- a/core/data/src/main/java/com/iguana/data/local/dao/RecordingDao.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.iguana.data.local.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.iguana.data.local.entity.PageTurnEventEntity -import kotlinx.coroutines.flow.Flow - -@Dao -interface RecordingDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertPageTurnEvent(event: PageTurnEventEntity) - - @Query("SELECT * FROM page_turn_events WHERE documentId = :documentId") - fun getPageTurnEvents(documentId: Long): Flow> - - @Query("DELETE FROM page_turn_events WHERE documentId = :documentId") - suspend fun deletePageTurnEvents(documentId: Long): Integer -} - diff --git a/core/data/src/main/java/com/iguana/data/local/db/RecordingDatabase.kt b/core/data/src/main/java/com/iguana/data/local/db/RecordingDatabase.kt deleted file mode 100644 index 68c6780..0000000 --- a/core/data/src/main/java/com/iguana/data/local/db/RecordingDatabase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.iguana.data.local.db - -import androidx.room.Database -import androidx.room.RoomDatabase -import com.iguana.data.local.dao.RecordingDao -import com.iguana.data.local.entity.PageTurnEventEntity - -@Database(entities = [PageTurnEventEntity::class], version = 1, exportSchema = false) -abstract class RecordingDatabase : RoomDatabase() { - abstract fun recordingDao(): RecordingDao -} \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt b/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt deleted file mode 100644 index 34d31f7..0000000 --- a/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.iguana.data.local.entity - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "page_turn_events") -data class PageTurnEventEntity( - @PrimaryKey(autoGenerate = true) val id: Long = 0, - val documentId: Long, - val prevPage: Int, - val nextPage: Int, - val timestamp: Double -) diff --git a/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt b/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt index 1cd6804..46a244f 100644 --- a/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt +++ b/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt @@ -1,5 +1,6 @@ package com.iguana.data.local.files +import android.util.Log import com.iguana.data.remote.model.PageTurnEventDto import com.iguana.domain.utils.AppError import java.io.File @@ -16,16 +17,17 @@ class RecordingFileStorage @Inject constructor( } // 페이지 이동 이벤트 저장 - fun savePageTurnEvents(recordingId: Long, events: List) { - val file = File(baseDir, "page_turn_events_$recordingId.txt") - file.writeText(events.joinToString(separator = "\n") { event -> + fun savePageTurnEvents(documentId: Long, events: List) { + val file = File(baseDir, "page_turn_events_$documentId.txt") + // 기존 파일 내용에 이어서 새 이벤트를 추가 + file.appendText(events.joinToString(separator = "\n") { event -> "${event.prevPage},${event.nextPage},${event.timestamp}" - }) + } + "\n") } // 로컬 스토리지에서 페이지 이동 이벤트 삭제 - fun deletePageTurnEvents(recordingId: Long) { - val file = File(baseDir, "page_turn_events_$recordingId.txt") + fun deletePageTurnEvents(documentId: Long) { + val file = File(baseDir, "page_turn_events_$documentId.txt") if (file.exists()) { file.delete() Result.success(Unit) diff --git a/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt b/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt index c0a8a9c..5ae0820 100644 --- a/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt +++ b/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt @@ -15,21 +15,28 @@ import java.util.Base64 fun List.toPageTurnEventRequestDto(recordingId: Long): PageTurnEventRequestDto { return PageTurnEventRequestDto( recordingId = recordingId, - events = this.toPageTurnEventDtoList() + events = this.map { event -> + PageTurnEventDto( + prevPage = event.prevPage, + nextPage = event.nextPage, + timestamp = event.timestamp + ) + } ) } + +// 확장 함수 정의 fun List.toPageTurnEventDtoList(): List { return this.map { event -> PageTurnEventDto( - prevPage = event.pageNumber - 1, - nextPage = event.pageNumber, + prevPage = event.prevPage, + nextPage = event.nextPage, timestamp = event.timestamp.toDouble() ) } } - // RecordingFile을 RecordingUploadRequestDto로 변환하는 함수 (녹음 파일 업로드) @RequiresApi(Build.VERSION_CODES.O) diff --git a/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt b/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt index d75a802..4c5c838 100644 --- a/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt +++ b/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.iguana.data.repository +import android.util.Log import com.iguana.data.local.files.RecordingFileStorage import com.iguana.data.mapper.toPageTurnEventDtoList import com.iguana.data.mapper.toPageTurnEventRequestDto @@ -71,11 +72,11 @@ class RecordRepositoryImpl @Inject constructor( } // 로컬 스토리지에 페이지 이동 이벤트 저장 - override suspend fun savePageTurnEvents(recordingId: Long, events: List) { + override suspend fun savePageTurnEvents(recordingId: Long, event: PageTurnEvent) { withContext(Dispatchers.IO) { localStorage.savePageTurnEvents( recordingId, - events.toPageTurnEventDtoList() + listOf(event).toPageTurnEventDtoList() ) // 페이지 이동 이벤트 로컬에 저장 } } diff --git a/core/domain/src/main/java/com/iguana/domain/model/record/PageTurnEvent.kt b/core/domain/src/main/java/com/iguana/domain/model/record/PageTurnEvent.kt index 9b987c5..25bac43 100644 --- a/core/domain/src/main/java/com/iguana/domain/model/record/PageTurnEvent.kt +++ b/core/domain/src/main/java/com/iguana/domain/model/record/PageTurnEvent.kt @@ -1,7 +1,8 @@ package com.iguana.domain.model.record -data class PageTurnEvent ( +data class PageTurnEvent( val documentId: Long, - val pageNumber: Int, - val timestamp: Long + val prevPage : Int, + val nextPage : Int, + val timestamp: Double ) \ No newline at end of file diff --git a/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt b/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt index f8e6c63..620b0c3 100644 --- a/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt +++ b/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt @@ -17,7 +17,7 @@ interface RecordRepository { suspend fun uploadPageTurnEvents(recordingId: Long, events: List) // 로컬 스토리지에 페이지 이동 이벤트 저장 - suspend fun savePageTurnEvents(recordingId: Long, events: List) + suspend fun savePageTurnEvents(recordingId: Long, event: PageTurnEvent) // 로컬에 저장된 페이지 이동 이벤트 삭제 suspend fun deletePageTurnEvents(recordingId: Long) diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventsUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventsUseCase.kt new file mode 100644 index 0000000..a59f9d8 --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventsUseCase.kt @@ -0,0 +1,29 @@ +package com.iguana.domain.usecase + +import android.util.Log +import com.iguana.domain.model.record.PageTurnEvent +import com.iguana.domain.repository.RecordRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class SavePageTurnEventUseCase @Inject constructor( + private val recordRepository: RecordRepository +) { + // 시작 시각을 전달받아 이동 시점에서의 경과 시간을 초 단위로 계산하여 이벤트 저장 + suspend operator fun invoke(documentId: Long, prevPage: Int?, currentPage: Int, startTimeMillis: Long) { + // 현재 시간과 녹음 시작 시간을 바탕으로 초 단위 타임스탬프 계산 + val timestamp = (System.currentTimeMillis() - startTimeMillis) / 1000.0 + + // PageTurnEvent 생성 + val event = PageTurnEvent( + documentId = documentId, + prevPage = prevPage ?: currentPage, + nextPage = currentPage, + timestamp = timestamp + ) + + // Repository를 통해 이벤트 저장 + recordRepository.savePageTurnEvents(documentId, event) + } +} diff --git a/feature/notetaking/src/main/AndroidManifest.xml b/feature/notetaking/src/main/AndroidManifest.xml index a44089f..a575235 100644 --- a/feature/notetaking/src/main/AndroidManifest.xml +++ b/feature/notetaking/src/main/AndroidManifest.xml @@ -13,7 +13,6 @@ - \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt index 61ac47d..51a6607 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt @@ -58,7 +58,7 @@ class RecordFragment() : Fragment() { } - // 페이지 번호 업데이트 메서드 + // 페이지 번호 업데이트 메서드 -> 페이지 이동 이벤트 발생시 상위 프래그먼트에서 호출되는 함수 fun updateContentForPage(pageNumber: Int) { if (isAdded && !isDetached) { // Fragment가 활성 상태인지 확인 viewModel.setPageNumber(pageNumber+1) diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt index cb63721..7eb83e0 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt @@ -8,29 +8,44 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.iguana.domain.usecase.SavePageTurnEventUseCase import com.iguana.domain.usecase.UploadPageTurnEventsUseCase import com.iguana.domain.usecase.UploadRecordingUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class RecordViewModel @Inject constructor( handle: SavedStateHandle, private val uploadRecordingUseCase: UploadRecordingUseCase, - private val uploadPageTurnEventsUseCase: UploadPageTurnEventsUseCase + private val uploadPageTurnEventsUseCase: UploadPageTurnEventsUseCase, + private val savePageTurnEventUseCase: SavePageTurnEventUseCase ) : ViewModel() { var documentId: Long = -1L private var recorder: MediaRecorder? = null - private var isRecording = false // 녹음 상태 확인 변수 + private val _recordingStatus = MutableLiveData() val recordingStatus: LiveData get() = _recordingStatus - private val _pageNumber = MutableLiveData() + private var startTimeMillis: Long = 0L + + private var prevPage: Int = 1 // 이전 페이지 번호 추적 + private val _pageNumber = MutableLiveData() // 현재 페이지 번호 val pageNumber: LiveData get() = _pageNumber fun setPageNumber(pageNumber: Int) { _pageNumber.value = pageNumber + if (isRecording()) { + viewModelScope.launch { + savePageTurnEvent(pageNumber) + } + prevPage = pageNumber + } else { + Log.d("RecordViewModel", "녹음 상태가 아님, 저장하지 않음") + } } fun startRecording(context: Context) { @@ -39,7 +54,8 @@ class RecordViewModel @Inject constructor( action = "START_RECORDING" } context.startService(intent) - _recordingStatus.value = true + _recordingStatus.value = true // 녹음 시작 전에 상태를 true로 설정 + startTimeMillis = System.currentTimeMillis() } catch (e: Exception) { Log.e("RecordViewModel", "녹음 시작 실패: ${e.message}") } @@ -52,4 +68,11 @@ class RecordViewModel @Inject constructor( context.startService(intent) _recordingStatus.value = false } + + private suspend fun savePageTurnEvent(currentPage: Int) { + savePageTurnEventUseCase(documentId, prevPage, currentPage, startTimeMillis) + } + private fun isRecording(): Boolean { + return recordingStatus.value ?: false + } } \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt index 1e8fb0b..58c07ad 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt @@ -1,17 +1,21 @@ package com.iguana.notetaking.recording +import android.Manifest import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.media.MediaRecorder import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat import java.io.IOException + class RecordingService : Service() { private var recorder: MediaRecorder? = null @@ -38,31 +42,52 @@ class RecordingService : Service() { private fun startRecording() { Log.d("RecordingService", "녹음이 시작되기 바로 직전입니다.") + if (isRecording) { Log.w("RecordingService", "녹음이 이미 진행 중입니다.") return } - // 파일 경로 생성 + + // 권한 확인 + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + Log.e("RecordingService", "녹음 권한이 없습니다.") + return + } + val outputFilePath = getRecordingFilePath(this) + if (outputFilePath == null) { + Log.e("RecordingService", "녹음 파일 경로를 생성할 수 없습니다.") + return + } recorder = MediaRecorder().apply { - setAudioSource(MediaRecorder.AudioSource.MIC) - setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) - setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) - setOutputFile(outputFilePath) - try { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) + setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) + setOutputFile(outputFilePath) + prepare() start() isRecording = true startForeground(notificationId, createRecordingNotification("녹음 중...")) Log.d("RecordingService", "녹음이 시작되었습니다.") + } catch (e: IllegalStateException) { + Log.e("RecordingService", "설정 오류: ${e.message}") + releaseRecorder() } catch (e: IOException) { - Log.e("RecordingService", "prepare() failed: ${e.message}") + Log.e("RecordingService", "prepare() 실패: ${e.message}") + releaseRecorder() } } } + private fun releaseRecorder() { + recorder?.release() + recorder = null + isRecording = false + } + private fun stopRecording() { if (!isRecording) { Log.w("RecordingService", "현재 녹음이 진행 중이지 않습니다.") @@ -104,9 +129,10 @@ class RecordingService : Service() { return null } - fun getRecordingFilePath(context: Context): String { + private fun getRecordingFilePath(context: Context): String { // 외부 저장소의 앱 전용 디렉토리 val directory = context.getExternalFilesDir(null) + Log.d("RecordingService", "녹음 파일 경로: ${directory?.absolutePath}") return "${directory?.absolutePath}/recording_${System.currentTimeMillis()}.3gp" } } From 11fa1597565092dc4c2494aecccc59411a05ae33 Mon Sep 17 00:00:00 2001 From: aengzu Date: Sun, 27 Oct 2024 12:21:10 +0900 Subject: [PATCH 09/19] =?UTF-8?q?Feat:=20=EB=85=B9=EC=9D=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *녹음 버튼 클릭 시 UI표시 -> 권한 확인 -> 녹음 시작 *RecordingService 를 통해 녹음 관리 *녹음 도중에 페이지 이동 이벤트 발생시 로컬에 파일로 기록 (이전페이지, 현재페이지, 현재시간-시작시간) *녹음 버튼 다시 클릭 시 녹음 종료 UI 표시 -> 녹음 로컬에 저장 -> 서버에 녹음 업로드 -> 로컬에 있는 페이지 이동 이벤트 파일 삭제 -> 로컬에 있는 녹음 파일 삭제 --- .../data/local/files/RecordingFileStorage.kt | 17 ++++- .../com/iguana/data/mapper/RecordMapper.kt | 18 +++-- .../data/repository/RecordRepositoryImpl.kt | 27 +++++-- .../domain/repository/RecordRepository.kt | 5 +- .../usecase/DeletePageTurnEventsUseCase.kt | 12 +++ .../domain/usecase/DeleteRecordingUseCase.kt | 12 +++ ...UseCase.kt => SavePageTurnEventUseCase.kt} | 5 -- .../usecase/UploadPageTurnEventsUseCase.kt | 3 +- .../domain/usecase/UploadRecordingUseCase.kt | 7 +- .../notetaking/recording/RecordViewModel.kt | 76 ++++++++++++++++++- .../notetaking/recording/RecordingService.kt | 32 +++++--- 11 files changed, 179 insertions(+), 35 deletions(-) create mode 100644 core/domain/src/main/java/com/iguana/domain/usecase/DeletePageTurnEventsUseCase.kt create mode 100644 core/domain/src/main/java/com/iguana/domain/usecase/DeleteRecordingUseCase.kt rename core/domain/src/main/java/com/iguana/domain/usecase/{SavePageTurnEventsUseCase.kt => SavePageTurnEventUseCase.kt} (81%) diff --git a/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt b/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt index 46a244f..7ec4d23 100644 --- a/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt +++ b/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt @@ -1,6 +1,5 @@ package com.iguana.data.local.files -import android.util.Log import com.iguana.data.remote.model.PageTurnEventDto import com.iguana.domain.utils.AppError import java.io.File @@ -41,4 +40,20 @@ class RecordingFileStorage @Inject constructor( val file = File(filePath) return file.exists() } + + // 페이지 이동 이벤트 로드 + fun loadPageTurnEvents(documentId: Long): List { + val file = File(baseDir, "page_turn_events_$documentId.txt") + if (!file.exists()) throw AppError.FileNotFound + + return file.readLines().map { line -> + val (prevPage, nextPage, timestamp) = line.split(",") + PageTurnEventDto( + prevPage = prevPage.toInt(), + nextPage = nextPage.toInt(), + timestamp = timestamp.toDouble() + ) + } + } + } diff --git a/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt b/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt index 5ae0820..31aa207 100644 --- a/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt +++ b/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt @@ -1,7 +1,5 @@ package com.iguana.data.mapper -import android.os.Build -import androidx.annotation.RequiresApi import com.iguana.data.remote.model.PageTurnEventDto import com.iguana.data.remote.model.PageTurnEventRequestDto import com.iguana.data.remote.model.RecordingUploadRequestDto @@ -25,8 +23,20 @@ fun List.toPageTurnEventRequestDto(recordingId: Long): PageTurnEv ) } +// PageTurnEvent(DTO List) -> PageTurnEvent(Domain List) -// 확장 함수 정의 +fun List.toPageTurnEventDomainList(documentId: Long): List { + return this.map { dto -> + PageTurnEvent( + documentId = documentId, + prevPage = dto.prevPage, + nextPage = dto.nextPage, + timestamp = dto.timestamp + ) + } +} + +// PageTurnEvents(Domain List) -> PageTurnEvents(DTO List) fun List.toPageTurnEventDtoList(): List { return this.map { event -> PageTurnEventDto( @@ -38,8 +48,6 @@ fun List.toPageTurnEventDtoList(): List { } // RecordingFile을 RecordingUploadRequestDto로 변환하는 함수 (녹음 파일 업로드) - -@RequiresApi(Build.VERSION_CODES.O) fun RecordingFile.toUploadRequestDto(): RecordingUploadRequestDto { // 파일을 Base64로 인코딩 val fileContent = File(this.filePath).readBytes() diff --git a/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt b/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt index 4c5c838..060525b 100644 --- a/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt +++ b/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.iguana.data.repository import android.util.Log import com.iguana.data.local.files.RecordingFileStorage +import com.iguana.data.mapper.toPageTurnEventDomainList import com.iguana.data.mapper.toPageTurnEventDtoList import com.iguana.data.mapper.toPageTurnEventRequestDto import com.iguana.data.mapper.toUploadRequestDto @@ -25,12 +26,20 @@ class RecordRepositoryImpl @Inject constructor( override suspend fun uploadRecordingFile(recordingFile: RecordingFile): RecordingFile { return withContext(Dispatchers.IO) { val uploadRequest = recordingFile.toUploadRequestDto() - val response = recordApi.uploadRecording(recordingFile.documentId ?: throw AppError.NullResponseError("Document ID가 없습니다."), uploadRequest) + Log.d("RecordRepositoryImpl", "Upload request 생성 완료: $uploadRequest") + val response = recordApi.uploadRecording( + recordingFile.documentId ?: throw AppError.NullResponseError("Document ID가 없습니다."), + uploadRequest + ) + Log.d("RecordRepositoryImpl", "API 응답 상태: ${response.isSuccessful}") if (response.isSuccessful) { - val body = response.body() ?: throw AppError.NullResponseError("녹음 파일 업로드 응답이 비어 있습니다.") + Log.d("RecordRepositoryImpl", "API 응답 상태: ${response.isSuccessful}") + val body = + response.body() ?: throw AppError.NullResponseError("녹음 파일 업로드 응답이 비어 있습니다.") return@withContext recordingFile.updateWithResponse(body) } else { + Log.e("RecordRepositoryImpl", "API 요청 실패: 코드 ${response.code()}, 메시지 ${response.message()}") throw AppError.UploadFailed } } @@ -48,10 +57,10 @@ class RecordRepositoryImpl @Inject constructor( } // 로컬에 있는 녹음 파일 삭제 - override suspend fun deleteRecordingFile(recordingFile: RecordingFile) { + override suspend fun deleteRecordingFile(filePath: String) { return withContext(Dispatchers.IO) { - if (localStorage.isFileExists(recordingFile.filePath)) { - val file = File(recordingFile.filePath) + if (localStorage.isFileExists(filePath)) { + val file = File(filePath) file.delete() } else { throw AppError.FileNotFound @@ -87,5 +96,13 @@ class RecordRepositoryImpl @Inject constructor( localStorage.deletePageTurnEvents(recordingId) // 페이지 이동 이벤트 삭제 } } + + // 로컬에서 모든 페이지 이동 이벤트를 로드 + override suspend fun loadPageTurnEvents(documentId: Long): List { + return withContext(Dispatchers.IO) { + localStorage.loadPageTurnEvents(documentId) + .toPageTurnEventDomainList(documentId) // 로컬에 저장된 페이지 이동 이벤트 로드 + } + } } diff --git a/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt b/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt index 620b0c3..45d4a7b 100644 --- a/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt +++ b/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt @@ -11,7 +11,7 @@ interface RecordRepository { suspend fun saveRecordingFile(recordingFile: RecordingFile) // 로컬에 있는 녹음 파일 삭제 - suspend fun deleteRecordingFile(recordingFile: RecordingFile) + suspend fun deleteRecordingFile(filePath: String) // 페이지 이동 이벤트 업로드 suspend fun uploadPageTurnEvents(recordingId: Long, events: List) @@ -22,4 +22,7 @@ interface RecordRepository { // 로컬에 저장된 페이지 이동 이벤트 삭제 suspend fun deletePageTurnEvents(recordingId: Long) + // 로컬에서 모든 페이지 이동 이벤트를 로드 + suspend fun loadPageTurnEvents(documentId: Long): List + } diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/DeletePageTurnEventsUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/DeletePageTurnEventsUseCase.kt new file mode 100644 index 0000000..63576c5 --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/DeletePageTurnEventsUseCase.kt @@ -0,0 +1,12 @@ +package com.iguana.domain.usecase + +import com.iguana.domain.repository.RecordRepository +import javax.inject.Inject + +class DeletePageTurnEventsUseCase @Inject constructor( + private val recordRepository: RecordRepository +) { + suspend operator fun invoke(documentId: Long) { + recordRepository.deletePageTurnEvents(documentId) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/DeleteRecordingUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/DeleteRecordingUseCase.kt new file mode 100644 index 0000000..61a8cc6 --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/DeleteRecordingUseCase.kt @@ -0,0 +1,12 @@ +package com.iguana.domain.usecase + +import com.iguana.domain.repository.RecordRepository +import javax.inject.Inject + +class DeleteRecordingUseCase @Inject constructor( + private val recordRepository: RecordRepository +) { + suspend operator fun invoke(filePath: String) { + recordRepository.deleteRecordingFile(filePath) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventsUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventUseCase.kt similarity index 81% rename from core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventsUseCase.kt rename to core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventUseCase.kt index a59f9d8..15dcc57 100644 --- a/core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventsUseCase.kt +++ b/core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventUseCase.kt @@ -1,16 +1,11 @@ package com.iguana.domain.usecase - -import android.util.Log import com.iguana.domain.model.record.PageTurnEvent import com.iguana.domain.repository.RecordRepository -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import javax.inject.Inject class SavePageTurnEventUseCase @Inject constructor( private val recordRepository: RecordRepository ) { - // 시작 시각을 전달받아 이동 시점에서의 경과 시간을 초 단위로 계산하여 이벤트 저장 suspend operator fun invoke(documentId: Long, prevPage: Int?, currentPage: Int, startTimeMillis: Long) { // 현재 시간과 녹음 시작 시간을 바탕으로 초 단위 타임스탬프 계산 val timestamp = (System.currentTimeMillis() - startTimeMillis) / 1000.0 diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt index 2a78a0b..24f5bce 100644 --- a/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt +++ b/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt @@ -7,7 +7,8 @@ import javax.inject.Inject class UploadPageTurnEventsUseCase @Inject constructor( private val recordRepository: RecordRepository ) { - suspend operator fun invoke(documentId: Long, recordingId: Long, events: List) { + suspend operator fun invoke(documentId: Long, recordingId: Long) { + val events = recordRepository.loadPageTurnEvents(documentId) recordRepository.uploadPageTurnEvents(recordingId, events) } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt index 1a33714..8620c35 100644 --- a/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt +++ b/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt @@ -1,5 +1,6 @@ package com.iguana.domain.usecase +import android.util.Log import com.iguana.domain.model.record.RecordingFile import com.iguana.domain.repository.RecordRepository import java.io.File @@ -12,9 +13,11 @@ class UploadRecordingUseCase @Inject constructor( documentId: Long, filePath: String, fileName: String - ): RecordingFile { + ): Long { val recordingFile = createRecordingFile(documentId, filePath, fileName) - return recordRepository.uploadRecordingFile(recordingFile) + val uploadedRecording = recordRepository.uploadRecordingFile(recordingFile) + + return uploadedRecording.recordingId ?: throw Exception("recordingId가 없습니다") } // RecordingFile 객체 생성 메서드 diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt index 7eb83e0..91738c5 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt @@ -1,7 +1,9 @@ package com.iguana.notetaking.recording +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.media.MediaRecorder import android.util.Log import androidx.lifecycle.LiveData @@ -9,19 +11,27 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.iguana.domain.usecase.DeletePageTurnEventsUseCase +import com.iguana.domain.usecase.DeleteRecordingUseCase import com.iguana.domain.usecase.SavePageTurnEventUseCase import com.iguana.domain.usecase.UploadPageTurnEventsUseCase import com.iguana.domain.usecase.UploadRecordingUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.launch import javax.inject.Inject +import javax.inject.Provider + @HiltViewModel class RecordViewModel @Inject constructor( handle: SavedStateHandle, private val uploadRecordingUseCase: UploadRecordingUseCase, private val uploadPageTurnEventsUseCase: UploadPageTurnEventsUseCase, - private val savePageTurnEventUseCase: SavePageTurnEventUseCase + private val savePageTurnEventUseCase: SavePageTurnEventUseCase, + private val deletePageTurnEventsUseCase: DeletePageTurnEventsUseCase, + private val deleteRecordingUseCase: DeleteRecordingUseCase, + @ApplicationContext private val contextProvider: Provider ) : ViewModel() { var documentId: Long = -1L private var recorder: MediaRecorder? = null @@ -35,6 +45,27 @@ class RecordViewModel @Inject constructor( private var prevPage: Int = 1 // 이전 페이지 번호 추적 private val _pageNumber = MutableLiveData() // 현재 페이지 번호 val pageNumber: LiveData get() = _pageNumber + private var filePath: String? = null + private var fileName: String? = null + + + private val context by lazy { contextProvider.get() } + + private val recordingReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + filePath = intent?.getStringExtra("filePath") + fileName = intent?.getStringExtra("fileName") + Log.d("RecordViewModel", "Recording finished: $filePath, $fileName") + + processRecordingAndEvents() + } + } + + init { + context.registerReceiver( + recordingReceiver, IntentFilter(BROADCAST_RECORDING_FINISHED) + ) + } fun setPageNumber(pageNumber: Int) { _pageNumber.value = pageNumber @@ -51,7 +82,7 @@ class RecordViewModel @Inject constructor( fun startRecording(context: Context) { try { val intent = Intent(context, RecordingService::class.java).apply { - action = "START_RECORDING" + action = ACTION_START_RECORDING } context.startService(intent) _recordingStatus.value = true // 녹음 시작 전에 상태를 true로 설정 @@ -62,17 +93,54 @@ class RecordViewModel @Inject constructor( } fun stopRecording(context: Context) { + sendStopIntent(context) + _recordingStatus.value = false + } + + private fun sendStopIntent(context: Context) { val intent = Intent(context, RecordingService::class.java).apply { - action = "STOP_RECORDING" + action = ACTION_STOP_RECORDING } context.startService(intent) - _recordingStatus.value = false + } + + private fun processRecordingAndEvents() { + viewModelScope.launch { + try { + // filePath 또는 fileName이 null일 경우 + if (filePath == null || fileName == null) { + Log.e("RecordViewModel", "filePath 또는 fileName이 null입니다. 업로드를 중단합니다.") + return@launch + } + + // TODO: 녹음 파일 업로드 및 페이지 이동 이벤트 업로드 -> 로컬에 저장된 파일 삭제 하는 부분인데 서버측 완료되면 주석해제 + // 1. 녹음 파일 업로드 + // val recordingId = uploadRecordingUseCase(documentId, filePath ?: return@launch, fileName ?: return@launch) + // 2. 페이지 이동 이벤트 업로드 + // uploadPageTurnEventsUseCase(documentId, recordingId) + // 3. 로컬에 저장된 페이지 이동 이벤트 파일 삭제 +// deletePageTurnEventsUseCase(documentId) + // 4. 로컬에 저장된 녹음 파일 삭제 +// deleteRecordingUseCase(filePath!!) + } catch (e: Exception) { + Log.e("RecordViewModel", "업로드 중 오류 발생: ${e.message}") + } + } } private suspend fun savePageTurnEvent(currentPage: Int) { savePageTurnEventUseCase(documentId, prevPage, currentPage, startTimeMillis) } + private fun isRecording(): Boolean { return recordingStatus.value ?: false } + + companion object { + private const val ACTION_START_RECORDING = "START_RECORDING" + private const val ACTION_STOP_RECORDING = "STOP_RECORDING" + private const val BROADCAST_RECORDING_FINISHED = "com.iguana.notetaking.RECORDING_FINISHED" + } + + } \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt index 58c07ad..f735eb6 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt @@ -22,6 +22,7 @@ class RecordingService : Service() { private var isRecording = false private val channelId = "RecordingServiceChannel" private val notificationId = 1 + private var outputFilePath: String? = null override fun onCreate() { super.onCreate() @@ -49,12 +50,15 @@ class RecordingService : Service() { } // 권한 확인 - if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission( + this, Manifest.permission.RECORD_AUDIO + ) != PackageManager.PERMISSION_GRANTED + ) { Log.e("RecordingService", "녹음 권한이 없습니다.") return } - val outputFilePath = getRecordingFilePath(this) + outputFilePath = getRecordingFilePath(this) if (outputFilePath == null) { Log.e("RecordingService", "녹음 파일 경로를 생성할 수 없습니다.") return @@ -71,7 +75,6 @@ class RecordingService : Service() { start() isRecording = true startForeground(notificationId, createRecordingNotification("녹음 중...")) - Log.d("RecordingService", "녹음이 시작되었습니다.") } catch (e: IllegalStateException) { Log.e("RecordingService", "설정 오류: ${e.message}") releaseRecorder() @@ -101,24 +104,20 @@ class RecordingService : Service() { recorder = null isRecording = false stopForeground(true) + sendRecordingFinishedBroadcast() stopSelf() - Log.d("RecordingService", "녹음이 종료되었습니다.") } private fun createRecordingNotification(contentText: String): Notification { - return NotificationCompat.Builder(this, channelId) - .setContentTitle("녹음 서비스") + return NotificationCompat.Builder(this, channelId).setContentTitle("녹음 서비스") .setContentText(contentText) .setSmallIcon(com.iguana.designsystem.R.drawable.ic_record_active) - .setPriority(NotificationCompat.PRIORITY_LOW) - .build() + .setPriority(NotificationCompat.PRIORITY_LOW).build() } private fun createNotificationChannel() { val channel = NotificationChannel( - channelId, - "녹음 서비스 채널", - NotificationManager.IMPORTANCE_LOW + channelId, "녹음 서비스 채널", NotificationManager.IMPORTANCE_LOW ) val notificationManager = getSystemService(NotificationManager::class.java) @@ -135,4 +134,15 @@ class RecordingService : Service() { Log.d("RecordingService", "녹음 파일 경로: ${directory?.absolutePath}") return "${directory?.absolutePath}/recording_${System.currentTimeMillis()}.3gp" } + + private fun sendRecordingFinishedBroadcast() { + outputFilePath?.let { path -> + val intent = Intent("com.iguana.notetaking.RECORDING_FINISHED").apply { + putExtra("filePath", path) + putExtra("fileName", path.substringAfterLast("/")) + } + sendBroadcast(intent) + } + } + } From 1ccead11af9b8c457532d428e7f356651d79c43e Mon Sep 17 00:00:00 2001 From: Kjamm Date: Mon, 28 Oct 2024 23:04:27 +0900 Subject: [PATCH 10/19] =?UTF-8?q?Design=20:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/ui/build.gradle.kts | 1 + .../main/java/com/iguana/ui/BaseActivity.kt | 7 +++ .../com/iguana/ui/SideTabLayoutFragment.kt | 1 + feature/favorites/build.gradle.kts | 12 ++++ .../com/iguana/favorites/FavoritesFragment.kt | 28 ++++++++++ .../res/layout-sw600dp/fragment_favorites.xml | 55 +++++++++++++++++++ .../main/res/layout/fragment_favorites.xml | 55 +++++++++++++++++++ 7 files changed, 159 insertions(+) create mode 100644 feature/favorites/src/main/java/com/iguana/favorites/FavoritesFragment.kt create mode 100644 feature/favorites/src/main/res/layout-sw600dp/fragment_favorites.xml create mode 100644 feature/favorites/src/main/res/layout/fragment_favorites.xml diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 8ad0473..a1b8da0 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.feature.documents) implementation(projects.feature.settings) implementation(projects.feature.userInfo) + implementation(projects.feature.favorites) // Test dependencies androidTestImplementation(libs.androidx.test.ext) androidTestImplementation(libs.androidx.test.espresso.core) diff --git a/core/ui/src/main/java/com/iguana/ui/BaseActivity.kt b/core/ui/src/main/java/com/iguana/ui/BaseActivity.kt index a42dd2f..57188a8 100644 --- a/core/ui/src/main/java/com/iguana/ui/BaseActivity.kt +++ b/core/ui/src/main/java/com/iguana/ui/BaseActivity.kt @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.iguana.dashBoard.DashBoardFragment import com.iguana.documents.DocumentsFragment +import com.iguana.favorites.FavoritesFragment import com.iguana.settings.SettingsFragment import com.iguana.ui.databinding.ActivityBaseBinding import com.iguana.userinfo.UserInfoFragment @@ -49,4 +50,10 @@ class BaseActivity : AppCompatActivity() { .replace(R.id.content_frame, UserInfoFragment()) .commit() } + + fun showFavorites() { + supportFragmentManager.beginTransaction() + .replace(R.id.content_frame, FavoritesFragment()) + .commit() + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/iguana/ui/SideTabLayoutFragment.kt b/core/ui/src/main/java/com/iguana/ui/SideTabLayoutFragment.kt index b7751d4..66a0f9b 100644 --- a/core/ui/src/main/java/com/iguana/ui/SideTabLayoutFragment.kt +++ b/core/ui/src/main/java/com/iguana/ui/SideTabLayoutFragment.kt @@ -45,6 +45,7 @@ class SideTabLayoutFragment : Fragment() { fun onFavoritesClick() { _selectedItem.value = 2 + (activity as? BaseActivity)?.showFavorites() } fun onProfileClick() { diff --git a/feature/favorites/build.gradle.kts b/feature/favorites/build.gradle.kts index d9bf551..7e7458b 100644 --- a/feature/favorites/build.gradle.kts +++ b/feature/favorites/build.gradle.kts @@ -3,14 +3,26 @@ plugins { id("iguana.android.hilt") id("iguana.kotlin.hilt") id("iguana.android.feature") + alias(libs.plugins.kotlin.android) + id("kotlin-kapt") } android { namespace = "com.iguana.favorites" + + buildFeatures { + dataBinding = true + viewBinding = true + buildConfig = true + } } dependencies { implementation(projects.core.domain) implementation(projects.core.designsystem) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) // Test dependencies androidTestImplementation(libs.androidx.test.ext) diff --git a/feature/favorites/src/main/java/com/iguana/favorites/FavoritesFragment.kt b/feature/favorites/src/main/java/com/iguana/favorites/FavoritesFragment.kt new file mode 100644 index 0000000..af02885 --- /dev/null +++ b/feature/favorites/src/main/java/com/iguana/favorites/FavoritesFragment.kt @@ -0,0 +1,28 @@ +package com.iguana.favorites + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.iguana.favorites.databinding.FragmentFavoritesBinding + +class FavoritesFragment : Fragment() { + + private var _binding: FragmentFavoritesBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentFavoritesBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/feature/favorites/src/main/res/layout-sw600dp/fragment_favorites.xml b/feature/favorites/src/main/res/layout-sw600dp/fragment_favorites.xml new file mode 100644 index 0000000..ca50ec0 --- /dev/null +++ b/feature/favorites/src/main/res/layout-sw600dp/fragment_favorites.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/favorites/src/main/res/layout/fragment_favorites.xml b/feature/favorites/src/main/res/layout/fragment_favorites.xml new file mode 100644 index 0000000..ca50ec0 --- /dev/null +++ b/feature/favorites/src/main/res/layout/fragment_favorites.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From e7f80d671124599edb23d5f05219979e7c03eb34 Mon Sep 17 00:00:00 2001 From: aengzu Date: Wed, 30 Oct 2024 18:21:21 +0900 Subject: [PATCH 11/19] =?UTF-8?q?Refactor:=20AuthInterceptor=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=86=A0=ED=81=B0=20=EC=96=BB=EC=96=B4=EC=98=AC?= =?UTF-8?q?=EB=95=8C=20=20SharedPreferencesHelper=20=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/src/main/java/com/iguana/data/di/AuthInterceptor.kt | 5 +++-- core/data/src/main/java/com/iguana/data/di/NetworkModule.kt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/data/src/main/java/com/iguana/data/di/AuthInterceptor.kt b/core/data/src/main/java/com/iguana/data/di/AuthInterceptor.kt index 4e87f10..8de44a7 100644 --- a/core/data/src/main/java/com/iguana/data/di/AuthInterceptor.kt +++ b/core/data/src/main/java/com/iguana/data/di/AuthInterceptor.kt @@ -1,11 +1,12 @@ package com.iguana.data.di +import com.iguana.domain.repository.SharedPreferencesHelper import okhttp3.Interceptor import okhttp3.Response -class AuthInterceptor(private val tokenProvider: () -> String) : Interceptor { +class AuthInterceptor(private val sharedPreference: SharedPreferencesHelper) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { - val token = tokenProvider() + val token = sharedPreference.getAccessToken() val request = chain.request().newBuilder() .addHeader("Authorization", "Bearer $token") .build() diff --git a/core/data/src/main/java/com/iguana/data/di/NetworkModule.kt b/core/data/src/main/java/com/iguana/data/di/NetworkModule.kt index 5aae0b5..8cb5743 100644 --- a/core/data/src/main/java/com/iguana/data/di/NetworkModule.kt +++ b/core/data/src/main/java/com/iguana/data/di/NetworkModule.kt @@ -23,7 +23,7 @@ object NetworkModule { @Singleton fun provideOkHttpClient(sharedPreferencesHelper: SharedPreferencesHelper): OkHttpClient { return OkHttpClient.Builder() - .addInterceptor(AuthInterceptor { sharedPreferencesHelper.getAccessToken() ?: "" }) + .addInterceptor(AuthInterceptor(sharedPreferencesHelper)) .build() } From 31c93ff208612e57cc195c62019c1be0b5c7490f Mon Sep 17 00:00:00 2001 From: aengzu Date: Wed, 30 Oct 2024 18:24:08 +0900 Subject: [PATCH 12/19] =?UTF-8?q?Refactor:=20html=20=ED=8F=AC=EB=A7=A4?= =?UTF-8?q?=ED=8C=85=20=EC=8B=9C=20parseAsHtml=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/iguana/domain/model/ai/AIResult.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/domain/src/main/java/com/iguana/domain/model/ai/AIResult.kt b/core/domain/src/main/java/com/iguana/domain/model/ai/AIResult.kt index b277784..36a9e0a 100644 --- a/core/domain/src/main/java/com/iguana/domain/model/ai/AIResult.kt +++ b/core/domain/src/main/java/com/iguana/domain/model/ai/AIResult.kt @@ -1,6 +1,8 @@ package com.iguana.domain.model.ai import android.text.Html +import android.text.Spanned +import androidx.core.text.parseAsHtml data class AIResult( val documentId: Long, @@ -17,12 +19,12 @@ data class AIResult( get() = !problem.isNullOrEmpty() // HTML 포맷을 처리한 요약 반환 - val formattedSummary: CharSequence - get() = Html.fromHtml(summary?.takeIf { it.isNotEmpty() } ?: "요약 이용 불가") + val formattedSummary: Spanned + get() = (summary?.takeIf { it.isNotEmpty() } ?: "요약 이용 불가").parseAsHtml() // HTML 포맷을 처리한 문제 반환 - val formattedProblem: CharSequence - get() = Html.fromHtml(problem?.takeIf { it.isNotEmpty() } ?: "문제 이용 불가") + val formattedProblem: Spanned + get() = (problem?.takeIf { it.isNotEmpty() } ?: "문제 이용 불가").parseAsHtml() } From 3fe63a5d7d83130c53a1fcec1b9cb636ddad653c Mon Sep 17 00:00:00 2001 From: aengzu Date: Wed, 30 Oct 2024 19:15:51 +0900 Subject: [PATCH 13/19] =?UTF-8?q?Refactor:=20AIRepositoryImpl=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20Dispatcher.IO=20=EC=A0=9C=EA=B1=B0,=20API=20?= =?UTF-8?q?=EB=A6=AC=ED=84=B4=20=ED=83=80=EC=9E=85=20Result=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iguana/data/remote/api/SummarizeApi.kt | 10 +-- .../data/repository/AIRepositoryImpl.kt | 84 +++---------------- .../iguana/domain/repository/AIRepository.kt | 10 +-- 3 files changed, 20 insertions(+), 84 deletions(-) diff --git a/core/data/src/main/java/com/iguana/data/remote/api/SummarizeApi.kt b/core/data/src/main/java/com/iguana/data/remote/api/SummarizeApi.kt index 73d2526..8652bfe 100644 --- a/core/data/src/main/java/com/iguana/data/remote/api/SummarizeApi.kt +++ b/core/data/src/main/java/com/iguana/data/remote/api/SummarizeApi.kt @@ -17,31 +17,31 @@ interface SummarizeApi { @POST("/api/ai/llm") suspend fun requestSummarization( @Body request: SummarizeRequestDto - ): Response + ): SummarizeResponseDto // 요약 상태 확인 @GET("/api/ai/llm/status/{documentId}") suspend fun checkStatus( @Path("documentId") documentId: Long - ): Response + ): StatusCheckResponseDto // 요약 결과 조회 @GET("/api/ai/llm/results/{documentId}") suspend fun getSummarization( @Path("documentId") documentId: Long - ): Response + ): SummarizeResultsResponseDto // 요약 상태 확인 (페이지별) @GET("/api/ai/llm/status/{documentId}/{pageNumber}") suspend fun checkStatusByPage( @Path("documentId") documentId: Long, @Path("pageNumber") pageNumber: Int - ): Response + ): StatusCheckByPageResponseDto // 요약 결과 조회 (페이지별) @GET("/api/ai/llm/results/{documentId}/{pageNumber}") suspend fun getSummarizationByPage( @Path("documentId") documentId: Long, @Path("pageNumber") pageNumber: Int - ): Response + ): SummarizeResultsByPageResponseDto } \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/repository/AIRepositoryImpl.kt b/core/data/src/main/java/com/iguana/data/repository/AIRepositoryImpl.kt index 988015f..27a29bb 100644 --- a/core/data/src/main/java/com/iguana/data/repository/AIRepositoryImpl.kt +++ b/core/data/src/main/java/com/iguana/data/repository/AIRepositoryImpl.kt @@ -15,93 +15,29 @@ import javax.inject.Inject class AIRepositoryImpl @Inject constructor( private val summarizeApi: SummarizeApi ): AIRepository { - override suspend fun requestSummarization(documentId: Long, pages: List): Result { - return withContext(Dispatchers.IO) { - try { - summarizeApi.requestSummarization(SummarizeRequestDto(documentId, pages)) - Result.success(Unit) - } catch (e: Exception) { - Result.failure(e) - } - } + override suspend fun requestSummarization(documentId: Long, pages: List) { + summarizeApi.requestSummarization(SummarizeRequestDto(documentId, pages)) } - override suspend fun checkStatus(documentId: Long): Result { - return withContext(Dispatchers.IO) { - try { - val response = summarizeApi.checkStatus(documentId) - if (response.isSuccessful) { - response.body()?.let { - return@withContext Result.success(it.toDomain()) // body가 null이 아니면 성공 반환 - } - // body가 null일 경우 실패 처리 - return@withContext Result.failure(AppError.NullResponseError("Response body is null")) - } - return@withContext Result.failure(AppError.UnknownError(response.message())) - } catch (e: Exception) { - // 네트워크 요청 실패 처리 - return@withContext Result.failure(AppError.NetworkError(e.hashCode())) - } - } + override suspend fun checkStatus(documentId: Long): AIStatusResult { + return summarizeApi.checkStatus(documentId).toDomain() } override suspend fun checkStatusByPage( documentId: Long, pageNumber: Int - ): Result { - return withContext(Dispatchers.IO) { - try { - val response = summarizeApi.checkStatusByPage(documentId, pageNumber) - if (response.isSuccessful) { - response.body()?.let { - return@withContext Result.success(it.toDomain()) - } - return@withContext Result.failure(AppError.NullResponseError("Response body is null")) - } - return@withContext Result.failure(AppError.UnknownError(response.message())) - } catch (e: Exception) { - return@withContext Result.failure(AppError.NetworkError(e.hashCode())) - } - } + ): AIStatusResultByPage { + return summarizeApi.checkStatusByPage(documentId, pageNumber).toDomain() } - - override suspend fun getSummarization(documentId: Long): Result> { - return withContext(Dispatchers.IO) { - try { - val response = summarizeApi.getSummarization(documentId) - if (response.isSuccessful) { - response.body()?.let { - return@withContext Result.success(it.toDomain()) - } - return@withContext Result.failure(AppError.NullResponseError("Response body is null")) - } - return@withContext Result.failure(AppError.UnknownError(response.message())) - - } - catch (e: Exception) { - return@withContext Result.failure(AppError.NetworkError(e.hashCode())) - } - } + override suspend fun getSummarization(documentId: Long): List { + return summarizeApi.getSummarization(documentId).toDomain() } override suspend fun getSummarizationByPage( documentId: Long, pageNumber: Int - ): Result { - return withContext(Dispatchers.IO) { - try { - val response = summarizeApi.getSummarizationByPage(documentId, pageNumber) - if (response.isSuccessful) { - response.body()?.let { - return@withContext Result.success(it.toDomain(documentId, pageNumber)) - } - return@withContext Result.failure(AppError.NullResponseError("Response body is null")) - } - return@withContext Result.failure(AppError.UnknownError(response.message())) - } catch (e: Exception) { - return@withContext Result.failure(AppError.NetworkError(e.hashCode())) - } - } + ): AIResult { + return summarizeApi.getSummarizationByPage(documentId, pageNumber).toDomain(documentId, pageNumber) } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/iguana/domain/repository/AIRepository.kt b/core/domain/src/main/java/com/iguana/domain/repository/AIRepository.kt index 74f99ea..d63b110 100644 --- a/core/domain/src/main/java/com/iguana/domain/repository/AIRepository.kt +++ b/core/domain/src/main/java/com/iguana/domain/repository/AIRepository.kt @@ -6,18 +6,18 @@ import com.iguana.domain.model.ai.AIStatusResultByPage interface AIRepository { // 요약 생성 - suspend fun requestSummarization(documentId: Long, pages: List): Result + suspend fun requestSummarization(documentId: Long, pages: List) // 요약 상태 확인 - suspend fun checkStatus(documentId: Long): Result + suspend fun checkStatus(documentId: Long): AIStatusResult // 요약 상태 확인 (페이지별) - suspend fun checkStatusByPage(documentId: Long, pageNumber: Int): Result + suspend fun checkStatusByPage(documentId: Long, pageNumber: Int): AIStatusResultByPage // 요약 결과 조회 - suspend fun getSummarization(documentId: Long): Result> + suspend fun getSummarization(documentId: Long): List // 요약 결과 조회 (페이지별) - suspend fun getSummarizationByPage(documentId: Long, pageNumber: Int): Result + suspend fun getSummarizationByPage(documentId: Long, pageNumber: Int): AIResult } \ No newline at end of file From 2e7f00ad3808ccec8e39aae65e3357653355bc5a Mon Sep 17 00:00:00 2001 From: aengzu Date: Wed, 30 Oct 2024 22:32:39 +0900 Subject: [PATCH 14/19] =?UTF-8?q?Refactor:=20RoomDB=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/iguana/data/di/DataModule.kt | 6 ++++ .../com/iguana/data/local/db/AppDatabase.kt | 5 +++- .../data/local/entity/PageTurnEventDao.kt | 18 +++++++++++ .../data/local/entity/PageTurnEventEntity.kt | 13 ++++++++ .../com/iguana/data/mapper/RecordMapper.kt | 26 ++++++++++------ .../data/repository/RecordRepositoryImpl.kt | 30 +++++++++++-------- .../domain/repository/RecordRepository.kt | 2 +- .../usecase/UploadPageTurnEventsUseCase.kt | 2 +- 8 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventDao.kt create mode 100644 core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt diff --git a/core/data/src/main/java/com/iguana/data/di/DataModule.kt b/core/data/src/main/java/com/iguana/data/di/DataModule.kt index c709338..d1a0c34 100644 --- a/core/data/src/main/java/com/iguana/data/di/DataModule.kt +++ b/core/data/src/main/java/com/iguana/data/di/DataModule.kt @@ -6,6 +6,7 @@ import com.iguana.data.local.dao.RecentFileDao import com.iguana.data.local.db.AppDatabase import com.iguana.domain.repository.SharedPreferencesHelper import com.iguana.data.local.db.SharedPreferencesHelperImpl +import com.iguana.data.local.entity.PageTurnEventDao import com.iguana.data.local.files.FileHelperImpl import com.iguana.domain.utils.FileHelper import dagger.Binds @@ -50,6 +51,11 @@ abstract class DataModule { return appDatabase.recentFileDao() } + @Provides + fun providePageTurnEventDao(appDatabase: AppDatabase): PageTurnEventDao { + return appDatabase.pageTurnEventDao() + } + @Provides @Singleton fun provideBaseDir(@ApplicationContext context: Context): File { diff --git a/core/data/src/main/java/com/iguana/data/local/db/AppDatabase.kt b/core/data/src/main/java/com/iguana/data/local/db/AppDatabase.kt index c0e47e6..3097e78 100644 --- a/core/data/src/main/java/com/iguana/data/local/db/AppDatabase.kt +++ b/core/data/src/main/java/com/iguana/data/local/db/AppDatabase.kt @@ -6,12 +6,15 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.sqlite.db.SupportSQLiteDatabase import com.iguana.data.local.dao.RecentFileDao +import com.iguana.data.local.entity.PageTurnEventDao +import com.iguana.data.local.entity.PageTurnEventEntity import com.iguana.data.local.entity.RecentFileEntity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -@Database(entities = [RecentFileEntity::class], version = 1, exportSchema = false) +@Database(entities = [RecentFileEntity::class, PageTurnEventEntity::class], version = 1, exportSchema = false) abstract class AppDatabase : RoomDatabase() { abstract fun recentFileDao(): RecentFileDao + abstract fun pageTurnEventDao(): PageTurnEventDao } \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventDao.kt b/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventDao.kt new file mode 100644 index 0000000..1362140 --- /dev/null +++ b/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventDao.kt @@ -0,0 +1,18 @@ +package com.iguana.data.local.entity + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query + +@Dao +interface PageTurnEventDao { + + @Insert + fun insert(event: PageTurnEventEntity) + + @Query("SELECT * FROM page_turn_events WHERE documentId = :documentId") + fun getEventsByDocumentId(documentId: Long): List + + @Query("DELETE FROM page_turn_events WHERE documentId = :documentId") + fun deleteEventsByDocumentId(documentId: Long) : Int +} diff --git a/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt b/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt new file mode 100644 index 0000000..624c2c1 --- /dev/null +++ b/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventEntity.kt @@ -0,0 +1,13 @@ +package com.iguana.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "page_turn_events") +data class PageTurnEventEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val documentId: Long, + val prevPage: Int, + val nextPage: Int, + val timestamp: Double +) \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt b/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt index 31aa207..0647926 100644 --- a/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt +++ b/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt @@ -1,5 +1,6 @@ package com.iguana.data.mapper +import com.iguana.data.local.entity.PageTurnEventEntity import com.iguana.data.remote.model.PageTurnEventDto import com.iguana.data.remote.model.PageTurnEventRequestDto import com.iguana.data.remote.model.RecordingUploadRequestDto @@ -36,15 +37,22 @@ fun List.toPageTurnEventDomainList(documentId: Long): List PageTurnEvents(DTO List) -fun List.toPageTurnEventDtoList(): List { - return this.map { event -> - PageTurnEventDto( - prevPage = event.prevPage, - nextPage = event.nextPage, - timestamp = event.timestamp.toDouble() - ) - } +fun PageTurnEvent.toEntity(documentId: Long): PageTurnEventEntity { + return PageTurnEventEntity( + documentId = documentId, + prevPage = prevPage, + nextPage = nextPage, + timestamp = timestamp + ) +} + +fun PageTurnEventEntity.toDomain(): PageTurnEvent { + return PageTurnEvent( + documentId = this.documentId, + prevPage = this.prevPage, + nextPage = this.nextPage, + timestamp = this.timestamp + ) } // RecordingFile을 RecordingUploadRequestDto로 변환하는 함수 (녹음 파일 업로드) diff --git a/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt b/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt index 060525b..f852040 100644 --- a/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt +++ b/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt @@ -1,9 +1,10 @@ package com.iguana.data.repository import android.util.Log +import com.iguana.data.local.entity.PageTurnEventDao import com.iguana.data.local.files.RecordingFileStorage -import com.iguana.data.mapper.toPageTurnEventDomainList -import com.iguana.data.mapper.toPageTurnEventDtoList +import com.iguana.data.mapper.toDomain +import com.iguana.data.mapper.toEntity import com.iguana.data.mapper.toPageTurnEventRequestDto import com.iguana.data.mapper.toUploadRequestDto import com.iguana.data.mapper.updateWithResponse @@ -19,7 +20,8 @@ import javax.inject.Inject class RecordRepositoryImpl @Inject constructor( private val recordApi: RecordApi, // 서버 통신을 위한 API 인터페이스 - private val localStorage: RecordingFileStorage // 로컬 스토리지 처리 클래스 + private val localStorage: RecordingFileStorage, // 로컬 스토리지 처리 클래스 + private val pageTurnEventDao: PageTurnEventDao ) : RecordRepository { // 서버에 녹음 파일 업로드 @@ -69,39 +71,41 @@ class RecordRepositoryImpl @Inject constructor( } // 서버에 페이지 이동 이벤트 업로드 - override suspend fun uploadPageTurnEvents(recordingId: Long, events: List) { + override suspend fun uploadPageTurnEvents(recordingId: Long, documentId: Long, events: List) { return withContext(Dispatchers.IO) { val requestDto = events.toPageTurnEventRequestDto(recordingId) // 도메인 모델을 DTO로 변환 val response = recordApi.recordPageTurnEvent(recordingId, requestDto) if (!response.isSuccessful) { throw AppError.PageTurnEventUploadFailed(response.code()) + } else { + deletePageTurnEvents(documentId) // 업로드 후 삭제 } } } // 로컬 스토리지에 페이지 이동 이벤트 저장 - override suspend fun savePageTurnEvents(recordingId: Long, event: PageTurnEvent) { + override suspend fun savePageTurnEvents(documentId: Long, event: PageTurnEvent) { withContext(Dispatchers.IO) { - localStorage.savePageTurnEvents( - recordingId, - listOf(event).toPageTurnEventDtoList() - ) // 페이지 이동 이벤트 로컬에 저장 + withContext(Dispatchers.IO) { + pageTurnEventDao.insert(event.toEntity(documentId)) + } } } // 로컬에 저장된 페이지 이동 이벤트 삭제 - override suspend fun deletePageTurnEvents(recordingId: Long) { + override suspend fun deletePageTurnEvents(documentId: Long) { withContext(Dispatchers.IO) { - localStorage.deletePageTurnEvents(recordingId) // 페이지 이동 이벤트 삭제 + pageTurnEventDao.deleteEventsByDocumentId(documentId) // 페이지 이동 이벤트 삭제 } } // 로컬에서 모든 페이지 이동 이벤트를 로드 override suspend fun loadPageTurnEvents(documentId: Long): List { return withContext(Dispatchers.IO) { - localStorage.loadPageTurnEvents(documentId) - .toPageTurnEventDomainList(documentId) // 로컬에 저장된 페이지 이동 이벤트 로드 + pageTurnEventDao.getEventsByDocumentId(documentId).map { + it.toDomain() + } } } } diff --git a/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt b/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt index 45d4a7b..3f30f78 100644 --- a/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt +++ b/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt @@ -14,7 +14,7 @@ interface RecordRepository { suspend fun deleteRecordingFile(filePath: String) // 페이지 이동 이벤트 업로드 - suspend fun uploadPageTurnEvents(recordingId: Long, events: List) + suspend fun uploadPageTurnEvents(documentId: Long, recordingId: Long, events: List) // 로컬 스토리지에 페이지 이동 이벤트 저장 suspend fun savePageTurnEvents(recordingId: Long, event: PageTurnEvent) diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt index 24f5bce..5e47c7f 100644 --- a/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt +++ b/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt @@ -9,6 +9,6 @@ class UploadPageTurnEventsUseCase @Inject constructor( ) { suspend operator fun invoke(documentId: Long, recordingId: Long) { val events = recordRepository.loadPageTurnEvents(documentId) - recordRepository.uploadPageTurnEvents(recordingId, events) + recordRepository.uploadPageTurnEvents(documentId, recordingId, events) } } \ No newline at end of file From c45e42a42d746076980a5ddf14054dd8f821d132 Mon Sep 17 00:00:00 2001 From: aengzu Date: Wed, 30 Oct 2024 23:04:17 +0900 Subject: [PATCH 15/19] =?UTF-8?q?Refactor:=20=EB=85=B9=EC=9D=8C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=A0=84=EB=8B=AC=20=EC=8B=9C=20SharedVie?= =?UTF-8?q?wModel=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/RecordRepository.kt | 2 +- .../iguana/notetaking/NotetakingActivity.kt | 117 ++++++++---------- .../iguana/notetaking/NotetakingViewModel.kt | 44 +++---- .../notetaking/recording/RecordFragment.kt | 9 +- .../notetaking/recording/RecordViewModel.kt | 4 +- .../notetaking/sidebar/SideBarFragment.kt | 13 -- .../layout-sw600dp/activity_notetaking.xml | 2 +- .../main/res/layout/activity_notetaking.xml | 2 +- 8 files changed, 79 insertions(+), 114 deletions(-) diff --git a/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt b/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt index 3f30f78..22102f5 100644 --- a/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt +++ b/core/domain/src/main/java/com/iguana/domain/repository/RecordRepository.kt @@ -20,7 +20,7 @@ interface RecordRepository { suspend fun savePageTurnEvents(recordingId: Long, event: PageTurnEvent) // 로컬에 저장된 페이지 이동 이벤트 삭제 - suspend fun deletePageTurnEvents(recordingId: Long) + suspend fun deletePageTurnEvents(documentId: Long) // 로컬에서 모든 페이지 이동 이벤트를 로드 suspend fun loadPageTurnEvents(documentId: Long): List diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt index d96748a..98cb729 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt @@ -25,11 +25,13 @@ class NotetakingActivity : AppCompatActivity() { const val PDF_TITLE_KEY = "PDF_TITLE" const val DEFAULT_TITLE = "무제" const val DOCUMENT_ID_KEY = "DOCUMENT_ID" + const val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 1001 } private lateinit var binding: ActivityNotetakingBinding private val viewModel: NotetakingViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -40,14 +42,17 @@ class NotetakingActivity : AppCompatActivity() { binding.lifecycleOwner = this initializeView() - observeToolbar() + observeViewModel() } // 뷰 초기화 메서드 private fun initializeView() { - viewModel.pdfUri = intent.getStringExtra(PDF_URI_KEY).toString() - viewModel.pdfTitle = intent.getStringExtra(PDF_TITLE_KEY) ?: DEFAULT_TITLE - viewModel.documentId = intent.getLongExtra(DOCUMENT_ID_KEY, -1) + viewModel.apply { + // PDF URI, 제목, 문서 ID 설정 + pdfUri = intent.getStringExtra(PDF_URI_KEY).toString() + pdfTitle = intent.getStringExtra(PDF_TITLE_KEY) ?: DEFAULT_TITLE + documentId = intent.getLongExtra(DOCUMENT_ID_KEY, -1) + } setupTitleBar() setupToolbar() @@ -62,35 +67,29 @@ class NotetakingActivity : AppCompatActivity() { // 툴바 설정 private fun setupToolbar() { - binding.toolbar.btnText.setOnClickListener { - val pdfViewerFragment = getPdfViewerFragment() - pdfViewerFragment?.getCurrentPdfPageFragment()?.addTextBox() - } - binding.toolbar.btnRecord.setOnClickListener { - requestAudioPermissions() - viewModel.toggleRecordTabActive() - } - binding.toolbar.btnAI.setOnClickListener { - viewModel.toggleAITabActive() + binding.toolbar.apply { + btnText.setOnClickListener { getPdfViewerFragment()?.getCurrentPdfPageFragment()?.addTextBox() } + btnRecord.setOnClickListener { handleRecordingPermissionAndToggle() } + btnAI.setOnClickListener { viewModel.toggleAI() } + } } - } // 타이틀바 설정 private fun setupTitleBar() { - binding.titleBar.backButton.setOnClickListener { - finish() + binding.titleBar.apply { + // 뒤로가기 버튼 클릭 시 액티비티 종료 + backButton.setOnClickListener { + finish() + } + // PDF 제목 설정 + titleBar.text = viewModel.pdfTitle } - binding.titleBar.titleBar.text = viewModel.pdfTitle } // PDF 및 사이드바 초기화 메서드 private fun setupPdfViewerAndSidebar() { - val pdfUri = Uri.parse(viewModel.pdfUri) - replaceFragment(R.id.pdf_fragment_container, PdfViewerFragment.newInstance(pdfUri)) - replaceFragment( - R.id.side_bar_container, - SideBarFragment.newInstance(viewModel.documentId, viewModel.pageNumber.value ?: 0) - ) + replaceFragment(R.id.pdf_fragment_container, PdfViewerFragment.newInstance(Uri.parse(viewModel.pdfUri))) + replaceFragment(R.id.side_bar_container, SideBarFragment.newInstance(viewModel.documentId, viewModel.pageNumber.value ?: 0)) } // 프래그먼트 교체 메서드 @@ -100,11 +99,6 @@ class NotetakingActivity : AppCompatActivity() { .commit() } - // PDF 에러 처리 메서드 - private fun handlePdfError(message: String) { - Log.e("NotetakingActivity", message) - Toast.makeText(this, "PDF 파일을 열 수 없습니다.", Toast.LENGTH_SHORT).show() - } // PDF 뷰어 프래그먼트 가져오기 메서드 private fun getPdfViewerFragment(): PdfViewerFragment? { @@ -123,57 +117,48 @@ class NotetakingActivity : AppCompatActivity() { } // 뷰모델의 상태를 관찰하여 UI 업데이트 - private fun observeToolbar() { - // Record 탭의 활성화 상태 관찰 - viewModel.isRecordActive.observe(this) { isActive -> - binding.toolbar.btnRecord.isSelected = isActive // 선택된 상태로 업데이트 - if (isActive) { - startRecordingInFragment() - Toast.makeText(this, "녹음이 시작되었습니다.", Toast.LENGTH_SHORT).show() - } else if (viewModel.isRecordingStopped()) { - stopRecordingInFragment() - Toast.makeText(this, "녹음이 종료되었습니다.", Toast.LENGTH_SHORT).show() - } + private fun observeViewModel() { + viewModel.isRecordingActive.observe(this) { isActive -> + binding.toolbar.btnRecord.isSelected = isActive + toastRecordingStatus(isActive) } - // AI 탭의 활성화 상태 관찰 viewModel.isAIActive.observe(this) { isActive -> - binding.toolbar.btnAI.isSelected = isActive // 선택된 상태로 업데이트 + binding.toolbar.btnAI.isSelected = isActive + } + } + + private fun handleRecordingPermissionAndToggle() { + if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + viewModel.toggleRecording() + } else { + requestAudioPermissions() } } - private val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 1001 + private fun toastRecordingStatus(isActive: Boolean) { + val message = if (isActive) { + "녹음이 시작되었습니다." + } else if (viewModel.isRecordingStopped()) { + "녹음이 종료되었습니다." + } else { + null + } + message?.let { Toast.makeText(this, it, Toast.LENGTH_SHORT).show() } + } // 권한 요청 메서드 private fun requestAudioPermissions() { - if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), RECORD_AUDIO_PERMISSION_REQUEST_CODE) - } else { - // 이미 권한이 있는 경우 - Log.d("NotetakingActivity", "이미 녹음 권한이 있습니다.") - } + requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), RECORD_AUDIO_PERMISSION_REQUEST_CODE) } // 권한 요청 결과 처리 override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - - } else { - Toast.makeText(this, "녹음 권한이 필요합니다.", Toast.LENGTH_SHORT).show() - } + if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + viewModel.toggleRecording() + } else { + Toast.makeText(this, "녹음 권한이 필요합니다.", Toast.LENGTH_SHORT).show() } } - // 녹음 시작 메서드 - private fun startRecordingInFragment() { - val sideBarFragment = getSideBarFragment() // SidebarFragment 가져오기 - sideBarFragment?.startRecordingInRecordFragment() // SidebarFragment에 녹음 시작 요청 - } - - // 녹음 중지 메서드 - private fun stopRecordingInFragment() { - val sideBarFragment = getSideBarFragment() // SidebarFragment 가져오기 - sideBarFragment?.stopRecordingInRecordFragment() // SidebarFragment에 녹음 중지 요청 - } } diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt index 2b4a8e4..9adfe25 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt @@ -12,12 +12,8 @@ import javax.inject.Inject class NotetakingViewModel @Inject constructor() : ViewModel() { var documentId: Long = -1L - set(value) = run { field = value } var pdfUri: String = "" - set(value) = run { field = value } - - var pdfTitle: String? = "무제" - set(value) = run { field = value } + var pdfTitle: String? = DEFAULT_TITLE private val _pageNumber = MutableLiveData() @@ -27,8 +23,8 @@ class NotetakingViewModel @Inject constructor() : ViewModel() { val isSideBarVisible: LiveData get() = _isSideBarVisible // record 버튼 활성화되어있는지 - private val _isRecordActive = MutableLiveData(false) - val isRecordActive: LiveData get() = _isRecordActive + private val _isRecordingActive = MutableLiveData(false) + val isRecordingActive: LiveData get() = _isRecordingActive // AI 버튼 활성화되어있는지 @@ -43,34 +39,34 @@ class NotetakingViewModel @Inject constructor() : ViewModel() { } // 사이드바의 가시성 상태를 토글하는 함수 - fun toggleSideBarVisibility() { + fun toggleSideBar() { _isSideBarVisible.value = _isSideBarVisible.value?.not() } - // 사이드바 보이게 하는 함수 - fun showSideBar() { - _isSideBarVisible.value = true - } - // 사이드바 숨기기 - fun hideSideBar() { - _isSideBarVisible.value = false + // 녹음 시작 및 종료 상태 변경 함수 + fun toggleRecording() { + wasRecording = _isRecordingActive.value == true + _isRecordingActive.value = !_isRecordingActive.value!! + showSideBar() } - // 액티브 상태를 토글 - fun toggleRecordTabActive() { - _isRecordActive.value = _isRecordActive.value?.not() - // 상태가 변경될 때 이전 녹음 상태를 저장 - wasRecording = _isRecordActive.value == true - showSideBar() - } - fun toggleAITabActive() { + fun toggleAI() { _isAIActive.value = _isAIActive.value?.not() showSideBar() } // 녹음이 종료되었는지 확인하는 함수 (이전에 녹음 중 -> 녹음 종료) fun isRecordingStopped(): Boolean { - return wasRecording && !_isRecordActive.value!! + return wasRecording && !_isRecordingActive.value!! + } + // 사이드바 보이기 + private fun showSideBar() { + _isSideBarVisible.value = true + } + + // 사이드바 숨기기 + fun hideSideBar() { + _isSideBarVisible.value = false } } diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt index 51a6607..baacddb 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordFragment.kt @@ -34,6 +34,7 @@ class RecordFragment() : Fragment() { private var _binding: FragmentRecordBinding? = null private val binding get() = _binding!! private val viewModel: RecordViewModel by viewModels() + private val sharedViewModel: NotetakingViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -81,12 +82,8 @@ class RecordFragment() : Fragment() { } private fun observeRecordingState() { - viewModel.recordingStatus.observe(viewLifecycleOwner) { isRecording -> - if (isRecording) { - binding.recordStatusTextView.text = "녹음 중" - } else { - binding.recordStatusTextView.text = "녹음 종료" - } + sharedViewModel.isRecordingActive.observe(viewLifecycleOwner) { isActive -> + if (isActive) startRecording(requireContext()) else stopRecording(requireContext()) } } diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt index 91738c5..983f734 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordViewModel.kt @@ -119,9 +119,9 @@ class RecordViewModel @Inject constructor( // 2. 페이지 이동 이벤트 업로드 // uploadPageTurnEventsUseCase(documentId, recordingId) // 3. 로컬에 저장된 페이지 이동 이벤트 파일 삭제 -// deletePageTurnEventsUseCase(documentId) + deletePageTurnEventsUseCase(documentId) // 4. 로컬에 저장된 녹음 파일 삭제 -// deleteRecordingUseCase(filePath!!) + deleteRecordingUseCase(filePath!!) } catch (e: Exception) { Log.e("RecordViewModel", "업로드 중 오류 발생: ${e.message}") } diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/sidebar/SideBarFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/sidebar/SideBarFragment.kt index b04d3a9..caa6a97 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/sidebar/SideBarFragment.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/sidebar/SideBarFragment.kt @@ -74,19 +74,6 @@ } } } - // RecordFragment에서 녹음 시작 - fun startRecordingInRecordFragment() { - (binding.sideBarViewPager.adapter as SidebarAdapter).getFragment(0)?.let { - (it as RecordFragment).startRecording(requireContext()) - } - } - - // RecordFragment에서 녹음 중지 - fun stopRecordingInRecordFragment() { - (binding.sideBarViewPager.adapter as SidebarAdapter).getFragment(0)?.let { - (it as RecordFragment).stopRecording(requireContext()) - } - } // 사이드바 내용 업데이트 메서드 fun updatePageNumber(pageNumber: Int) { diff --git a/feature/notetaking/src/main/res/layout-sw600dp/activity_notetaking.xml b/feature/notetaking/src/main/res/layout-sw600dp/activity_notetaking.xml index fae7774..65a3226 100644 --- a/feature/notetaking/src/main/res/layout-sw600dp/activity_notetaking.xml +++ b/feature/notetaking/src/main/res/layout-sw600dp/activity_notetaking.xml @@ -68,7 +68,7 @@ android:layout_margin="0dp" android:layout_marginTop="72dp" android:background="?attr/selectableItemBackground" - android:onClick="@{() -> viewModel.toggleSideBarVisibility()}" + android:onClick="@{() -> viewModel.toggleSideBar()}" android:src="@drawable/slider_selector" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/side_bar_container" diff --git a/feature/notetaking/src/main/res/layout/activity_notetaking.xml b/feature/notetaking/src/main/res/layout/activity_notetaking.xml index c32d276..774a7dc 100644 --- a/feature/notetaking/src/main/res/layout/activity_notetaking.xml +++ b/feature/notetaking/src/main/res/layout/activity_notetaking.xml @@ -65,7 +65,7 @@ android:layout_margin="0dp" android:layout_marginTop="72dp" android:background="?attr/selectableItemBackground" - android:onClick="@{() -> viewModel.toggleSideBarVisibility()}" + android:onClick="@{() -> viewModel.toggleSideBar()}" android:scaleType="centerCrop" android:src="@drawable/slider_selector" app:layout_constraintBottom_toBottomOf="parent" From 926fbf23f2d083bb1454ee7543659ce06d8add14 Mon Sep 17 00:00:00 2001 From: aengzu Date: Fri, 1 Nov 2024 02:03:40 +0900 Subject: [PATCH 16/19] =?UTF-8?q?Design:=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=8E=B8=EC=A7=91=20=EB=AA=A8=EB=93=9C=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/ic_at_array.xml | 15 +++++++++++++++ .../src/main/res/drawable/ic_at_back.xml | 9 +++++++++ .../src/main/res/drawable/ic_at_bold.xml | 9 +++++++++ .../src/main/res/drawable/ic_at_chevron_down.xml | 9 +++++++++ .../src/main/res/drawable/ic_at_italic.xml | 9 +++++++++ .../src/main/res/drawable/ic_at_next.xml | 9 +++++++++ .../src/main/res/drawable/ic_at_nums_array.xml | 9 +++++++++ .../main/res/drawable/ic_at_text_align_center.xml | 9 +++++++++ .../res/drawable/ic_at_text_align_justify.xml | 9 +++++++++ .../main/res/drawable/ic_at_text_align_left.xml | 9 +++++++++ .../main/res/drawable/ic_at_text_align_right.xml | 9 +++++++++ .../src/main/res/drawable/ic_at_text_delete.xml | 9 +++++++++ .../src/main/res/drawable/ic_at_underscore.xml | 9 +++++++++ 13 files changed, 123 insertions(+) create mode 100644 core/designsystem/src/main/res/drawable/ic_at_array.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_back.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_bold.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_chevron_down.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_italic.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_next.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_nums_array.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_text_align_center.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_text_align_justify.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_text_align_left.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_text_align_right.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_text_delete.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_underscore.xml diff --git a/core/designsystem/src/main/res/drawable/ic_at_array.xml b/core/designsystem/src/main/res/drawable/ic_at_array.xml new file mode 100644 index 0000000..18e1154 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_array.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_back.xml b/core/designsystem/src/main/res/drawable/ic_at_back.xml new file mode 100644 index 0000000..a67c6e9 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_bold.xml b/core/designsystem/src/main/res/drawable/ic_at_bold.xml new file mode 100644 index 0000000..8a13d9e --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_bold.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_chevron_down.xml b/core/designsystem/src/main/res/drawable/ic_at_chevron_down.xml new file mode 100644 index 0000000..4c37227 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_chevron_down.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_italic.xml b/core/designsystem/src/main/res/drawable/ic_at_italic.xml new file mode 100644 index 0000000..251dae9 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_italic.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_next.xml b/core/designsystem/src/main/res/drawable/ic_at_next.xml new file mode 100644 index 0000000..220c548 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_next.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_nums_array.xml b/core/designsystem/src/main/res/drawable/ic_at_nums_array.xml new file mode 100644 index 0000000..e6dfc0b --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_nums_array.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_text_align_center.xml b/core/designsystem/src/main/res/drawable/ic_at_text_align_center.xml new file mode 100644 index 0000000..bcc6823 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_text_align_center.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_text_align_justify.xml b/core/designsystem/src/main/res/drawable/ic_at_text_align_justify.xml new file mode 100644 index 0000000..cec902b --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_text_align_justify.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_text_align_left.xml b/core/designsystem/src/main/res/drawable/ic_at_text_align_left.xml new file mode 100644 index 0000000..651eb97 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_text_align_left.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_text_align_right.xml b/core/designsystem/src/main/res/drawable/ic_at_text_align_right.xml new file mode 100644 index 0000000..b77e11a --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_text_align_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_text_delete.xml b/core/designsystem/src/main/res/drawable/ic_at_text_delete.xml new file mode 100644 index 0000000..3f9220a --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_text_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_underscore.xml b/core/designsystem/src/main/res/drawable/ic_at_underscore.xml new file mode 100644 index 0000000..639f4cc --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_underscore.xml @@ -0,0 +1,9 @@ + + + From 4ee5fe0b00da97a4ff621b665808069ebae38590 Mon Sep 17 00:00:00 2001 From: aengzu Date: Fri, 1 Nov 2024 13:24:22 +0900 Subject: [PATCH 17/19] =?UTF-8?q?Design:=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=8E=B8=EC=A7=91=20=EB=B0=94=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/res/drawable/ic_at_color_black.xml | 9 ++++ .../res/drawable/ic_at_strike_through.xml | 9 ++++ .../com/iguana/notetaking/BindingAdapters.kt | 17 +++++++ .../iguana/notetaking/NotetakingActivity.kt | 11 ++++- .../iguana/notetaking/NotetakingViewModel.kt | 9 +++- .../layout-sw600dp/activity_notetaking.xml | 19 +++++-- .../main/res/layout/activity_notetaking.xml | 25 +++++++--- .../main/res/layout/text_align_dropdown.xml | 22 +++++++++ .../main/res/layout/text_color_dropdown.xml | 22 +++++++++ .../src/main/res/layout/text_edit_bar.xml | 49 +++++++++++++++++++ .../src/main/res/layout/text_format_icons.xml | 40 +++++++++++++++ .../src/main/res/layout/text_list_icons.xml | 23 +++++++++ .../main/res/layout/text_size_dropdown.xml | 30 ++++++++++++ .../src/main/res/layout/toolbar.xml | 7 ++- 14 files changed, 276 insertions(+), 16 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_at_color_black.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_at_strike_through.xml create mode 100644 feature/notetaking/src/main/java/com/iguana/notetaking/BindingAdapters.kt create mode 100644 feature/notetaking/src/main/res/layout/text_align_dropdown.xml create mode 100644 feature/notetaking/src/main/res/layout/text_color_dropdown.xml create mode 100644 feature/notetaking/src/main/res/layout/text_edit_bar.xml create mode 100644 feature/notetaking/src/main/res/layout/text_format_icons.xml create mode 100644 feature/notetaking/src/main/res/layout/text_list_icons.xml create mode 100644 feature/notetaking/src/main/res/layout/text_size_dropdown.xml diff --git a/core/designsystem/src/main/res/drawable/ic_at_color_black.xml b/core/designsystem/src/main/res/drawable/ic_at_color_black.xml new file mode 100644 index 0000000..18e6ee7 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_color_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_at_strike_through.xml b/core/designsystem/src/main/res/drawable/ic_at_strike_through.xml new file mode 100644 index 0000000..5f7b9a7 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_at_strike_through.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/BindingAdapters.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/BindingAdapters.kt new file mode 100644 index 0000000..0b1edce --- /dev/null +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/BindingAdapters.kt @@ -0,0 +1,17 @@ +package com.iguana.notetaking + +import android.view.View +import androidx.databinding.BindingAdapter + +class BindingAdapters{ + + companion object{ + @BindingAdapter("android:visibility") + @JvmStatic// it is important + fun setVisibility(target: View, visible: Boolean) { + target.visibility = if (visible) View.VISIBLE else View.GONE + } + + } + +} \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt index 98cb729..c2f28ce 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt @@ -5,6 +5,7 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle import android.util.Log +import android.view.View import android.view.WindowInsets.Side import android.widget.Toast import androidx.activity.enableEdgeToEdge @@ -68,7 +69,10 @@ class NotetakingActivity : AppCompatActivity() { // 툴바 설정 private fun setupToolbar() { binding.toolbar.apply { - btnText.setOnClickListener { getPdfViewerFragment()?.getCurrentPdfPageFragment()?.addTextBox() } + btnText.setOnClickListener { + viewModel.toggleTextMode() + // TODO 주석이 pdf 뷰어에 표시되도록 하는 로직 + } btnRecord.setOnClickListener { handleRecordingPermissionAndToggle() } btnAI.setOnClickListener { viewModel.toggleAI() } } @@ -126,6 +130,11 @@ class NotetakingActivity : AppCompatActivity() { viewModel.isAIActive.observe(this) { isActive -> binding.toolbar.btnAI.isSelected = isActive } + + viewModel.isTextMode.observe(this) { isTextMode -> + val textEditBar = binding.root.findViewById(R.id.text_edit_bar) + textEditBar.visibility = if (isTextMode) View.VISIBLE else View.GONE + } } private fun handleRecordingPermissionAndToggle() { diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt index 9adfe25..c586a74 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt @@ -34,6 +34,14 @@ class NotetakingViewModel @Inject constructor() : ViewModel() { // 이전 녹음 상태 추적 변수 private var wasRecording = false + private val _isTextMode = MutableLiveData(false) + val isTextMode: LiveData get() = _isTextMode + + // 텍스트 모드 토글 함수 + fun toggleTextMode() { + _isTextMode.value = _isTextMode.value?.not() + } + // 페이지 번호 설정 함수 fun setPageNumber(pageNumber: Int) { _pageNumber.value = pageNumber } @@ -50,7 +58,6 @@ class NotetakingViewModel @Inject constructor() : ViewModel() { showSideBar() } - fun toggleAI() { _isAIActive.value = _isAIActive.value?.not() showSideBar() diff --git a/feature/notetaking/src/main/res/layout-sw600dp/activity_notetaking.xml b/feature/notetaking/src/main/res/layout-sw600dp/activity_notetaking.xml index 65a3226..e291035 100644 --- a/feature/notetaking/src/main/res/layout-sw600dp/activity_notetaking.xml +++ b/feature/notetaking/src/main/res/layout-sw600dp/activity_notetaking.xml @@ -26,13 +26,12 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + @@ -43,10 +42,22 @@ android:layout_width="0dp" android:layout_height="0dp" android:background="@color/grey_90" + app:layout_constraintTop_toBottomOf="@id/toolbar" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/side_bar_container" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/toolbar" /> + app:layout_constraintStart_toStartOf="parent" /> + + + + + + @@ -30,9 +32,9 @@ layout="@layout/toolbar" android:layout_width="0dp" android:layout_height="wrap_content" - app:layout_constraintBottom_toTopOf="@id/pdf_fragment_container" app:layout_constraintEnd_toStartOf="@id/side_bar_container" - app:layout_constraintStart_toStartOf="parent" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/titleBar" /> + app:layout_constraintStart_toStartOf="parent" /> + + + - + - + + + + + + + diff --git a/feature/notetaking/src/main/res/layout/text_color_dropdown.xml b/feature/notetaking/src/main/res/layout/text_color_dropdown.xml new file mode 100644 index 0000000..418e57e --- /dev/null +++ b/feature/notetaking/src/main/res/layout/text_color_dropdown.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/feature/notetaking/src/main/res/layout/text_edit_bar.xml b/feature/notetaking/src/main/res/layout/text_edit_bar.xml new file mode 100644 index 0000000..97c6ca3 --- /dev/null +++ b/feature/notetaking/src/main/res/layout/text_edit_bar.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/notetaking/src/main/res/layout/text_format_icons.xml b/feature/notetaking/src/main/res/layout/text_format_icons.xml new file mode 100644 index 0000000..452ceeb --- /dev/null +++ b/feature/notetaking/src/main/res/layout/text_format_icons.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + diff --git a/feature/notetaking/src/main/res/layout/text_list_icons.xml b/feature/notetaking/src/main/res/layout/text_list_icons.xml new file mode 100644 index 0000000..19c03ff --- /dev/null +++ b/feature/notetaking/src/main/res/layout/text_list_icons.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/feature/notetaking/src/main/res/layout/text_size_dropdown.xml b/feature/notetaking/src/main/res/layout/text_size_dropdown.xml new file mode 100644 index 0000000..6b90a70 --- /dev/null +++ b/feature/notetaking/src/main/res/layout/text_size_dropdown.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/feature/notetaking/src/main/res/layout/toolbar.xml b/feature/notetaking/src/main/res/layout/toolbar.xml index 46ffecd..5ed7cf2 100644 --- a/feature/notetaking/src/main/res/layout/toolbar.xml +++ b/feature/notetaking/src/main/res/layout/toolbar.xml @@ -1,6 +1,5 @@ - + - + - + Date: Fri, 1 Nov 2024 15:14:49 +0900 Subject: [PATCH 18/19] =?UTF-8?q?Feat:=20Annotation=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20Room=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/iguana/data/di/DataModule.kt | 8 +- .../iguana/data/local/dao/AnnotationDao.kt | 25 ++++ .../local/{entity => dao}/PageTurnEventDao.kt | 3 +- .../com/iguana/data/local/db/AppDatabase.kt | 14 +- .../data/local/entity/AnnotationEntity.kt | 15 ++ .../data/repository/RecordRepositoryImpl.kt | 2 +- .../com/iguana/notetaking/BindingAdapters.kt | 17 --- .../iguana/notetaking/NotetakingActivity.kt | 4 +- .../com/iguana/notetaking/PdfPageFragment.kt | 139 ------------------ .../com/iguana/notetaking/pdf/PdfEditor.kt | 89 +++++++++++ .../notetaking/{ => pdf}/PdfPageAdapter.kt | 2 +- .../iguana/notetaking/pdf/PdfPageFragment.kt | 87 +++++++++++ .../notetaking/{ => pdf}/PdfViewerFragment.kt | 4 +- .../{ => pdf}/PdfViewerViewModel.kt | 9 +- 14 files changed, 244 insertions(+), 174 deletions(-) create mode 100644 core/data/src/main/java/com/iguana/data/local/dao/AnnotationDao.kt rename core/data/src/main/java/com/iguana/data/local/{entity => dao}/PageTurnEventDao.kt (83%) create mode 100644 core/data/src/main/java/com/iguana/data/local/entity/AnnotationEntity.kt delete mode 100644 feature/notetaking/src/main/java/com/iguana/notetaking/BindingAdapters.kt delete mode 100644 feature/notetaking/src/main/java/com/iguana/notetaking/PdfPageFragment.kt create mode 100644 feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfEditor.kt rename feature/notetaking/src/main/java/com/iguana/notetaking/{ => pdf}/PdfPageAdapter.kt (92%) create mode 100644 feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageFragment.kt rename feature/notetaking/src/main/java/com/iguana/notetaking/{ => pdf}/PdfViewerFragment.kt (97%) rename feature/notetaking/src/main/java/com/iguana/notetaking/{ => pdf}/PdfViewerViewModel.kt (83%) diff --git a/core/data/src/main/java/com/iguana/data/di/DataModule.kt b/core/data/src/main/java/com/iguana/data/di/DataModule.kt index d1a0c34..e703f56 100644 --- a/core/data/src/main/java/com/iguana/data/di/DataModule.kt +++ b/core/data/src/main/java/com/iguana/data/di/DataModule.kt @@ -2,11 +2,12 @@ package com.iguana.data.di import android.content.Context import androidx.room.Room +import com.iguana.data.local.dao.AnnotationDao import com.iguana.data.local.dao.RecentFileDao import com.iguana.data.local.db.AppDatabase import com.iguana.domain.repository.SharedPreferencesHelper import com.iguana.data.local.db.SharedPreferencesHelperImpl -import com.iguana.data.local.entity.PageTurnEventDao +import com.iguana.data.local.dao.PageTurnEventDao import com.iguana.data.local.files.FileHelperImpl import com.iguana.domain.utils.FileHelper import dagger.Binds @@ -56,6 +57,11 @@ abstract class DataModule { return appDatabase.pageTurnEventDao() } + @Provides + fun provideAnnotationDao(appDatabase: AppDatabase): AnnotationDao { + return appDatabase.annotationDao() + } + @Provides @Singleton fun provideBaseDir(@ApplicationContext context: Context): File { diff --git a/core/data/src/main/java/com/iguana/data/local/dao/AnnotationDao.kt b/core/data/src/main/java/com/iguana/data/local/dao/AnnotationDao.kt new file mode 100644 index 0000000..6f2351c --- /dev/null +++ b/core/data/src/main/java/com/iguana/data/local/dao/AnnotationDao.kt @@ -0,0 +1,25 @@ +package com.iguana.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface AnnotationDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAnnotation(annotation: Annotation) + + @Query("SELECT * FROM annotations WHERE documentId = :documentId AND pageNumber = :pageNumber") + suspend fun getAnnotationsByPage(documentId: Long, pageNumber: Int): List + + @Query("DELETE FROM annotations WHERE id = :id") + suspend fun deleteAnnotation(id: Long) + + @Query("UPDATE annotations SET text = :text, xPosition = :xPosition, yPosition = :yPosition WHERE id = :id") + suspend fun updateAnnotation(id: Long, text: String, xPosition: Float, yPosition: Float) + + @Query("DELETE FROM annotations WHERE documentId = :documentId") + suspend fun deleteAnnotationsByDocument(documentId: Long) +} diff --git a/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventDao.kt b/core/data/src/main/java/com/iguana/data/local/dao/PageTurnEventDao.kt similarity index 83% rename from core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventDao.kt rename to core/data/src/main/java/com/iguana/data/local/dao/PageTurnEventDao.kt index 1362140..4b19a2e 100644 --- a/core/data/src/main/java/com/iguana/data/local/entity/PageTurnEventDao.kt +++ b/core/data/src/main/java/com/iguana/data/local/dao/PageTurnEventDao.kt @@ -1,8 +1,9 @@ -package com.iguana.data.local.entity +package com.iguana.data.local.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +import com.iguana.data.local.entity.PageTurnEventEntity @Dao interface PageTurnEventDao { diff --git a/core/data/src/main/java/com/iguana/data/local/db/AppDatabase.kt b/core/data/src/main/java/com/iguana/data/local/db/AppDatabase.kt index 3097e78..a4e98e6 100644 --- a/core/data/src/main/java/com/iguana/data/local/db/AppDatabase.kt +++ b/core/data/src/main/java/com/iguana/data/local/db/AppDatabase.kt @@ -1,20 +1,18 @@ package com.iguana.data.local.db -import android.content.Context import androidx.room.Database -import androidx.room.Room import androidx.room.RoomDatabase -import androidx.sqlite.db.SupportSQLiteDatabase +import com.iguana.data.local.dao.AnnotationDao import com.iguana.data.local.dao.RecentFileDao -import com.iguana.data.local.entity.PageTurnEventDao +import com.iguana.data.local.dao.PageTurnEventDao +import com.iguana.data.local.entity.AnnotationEntity import com.iguana.data.local.entity.PageTurnEventEntity import com.iguana.data.local.entity.RecentFileEntity -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -@Database(entities = [RecentFileEntity::class, PageTurnEventEntity::class], version = 1, exportSchema = false) +@Database(entities = [RecentFileEntity::class, PageTurnEventEntity::class, AnnotationEntity::class], version = 1, exportSchema = false) abstract class AppDatabase : RoomDatabase() { abstract fun recentFileDao(): RecentFileDao abstract fun pageTurnEventDao(): PageTurnEventDao + + abstract fun annotationDao(): AnnotationDao } \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/local/entity/AnnotationEntity.kt b/core/data/src/main/java/com/iguana/data/local/entity/AnnotationEntity.kt new file mode 100644 index 0000000..b6ac138 --- /dev/null +++ b/core/data/src/main/java/com/iguana/data/local/entity/AnnotationEntity.kt @@ -0,0 +1,15 @@ +package com.iguana.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + + +@Entity(tableName = "annotations") +data class AnnotationEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val documentId: Long, // 외래 키로 사용될 문서 ID + val pageNumber: Int, // 주석이 있는 페이지 번호 + val xPosition: Float, // x 좌표 + val yPosition: Float, // y 좌표 + val text: String // 주석 텍스트 +) \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt b/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt index f852040..b697189 100644 --- a/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt +++ b/core/data/src/main/java/com/iguana/data/repository/RecordRepositoryImpl.kt @@ -1,7 +1,7 @@ package com.iguana.data.repository import android.util.Log -import com.iguana.data.local.entity.PageTurnEventDao +import com.iguana.data.local.dao.PageTurnEventDao import com.iguana.data.local.files.RecordingFileStorage import com.iguana.data.mapper.toDomain import com.iguana.data.mapper.toEntity diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/BindingAdapters.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/BindingAdapters.kt deleted file mode 100644 index 0b1edce..0000000 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/BindingAdapters.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.iguana.notetaking - -import android.view.View -import androidx.databinding.BindingAdapter - -class BindingAdapters{ - - companion object{ - @BindingAdapter("android:visibility") - @JvmStatic// it is important - fun setVisibility(target: View, visible: Boolean) { - target.visibility = if (visible) View.VISIBLE else View.GONE - } - - } - -} \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt index c2f28ce..08b3f14 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt @@ -4,15 +4,13 @@ import android.Manifest import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle -import android.util.Log import android.view.View -import android.view.WindowInsets.Side import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.iguana.notetaking.databinding.ActivityNotetakingBinding -import com.iguana.notetaking.recording.RecordFragment +import com.iguana.notetaking.pdf.PdfViewerFragment import com.iguana.notetaking.sidebar.SideBarFragment import dagger.hilt.android.AndroidEntryPoint diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/PdfPageFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/PdfPageFragment.kt deleted file mode 100644 index d068b1f..0000000 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/PdfPageFragment.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.iguana.notetaking - -import android.net.Uri -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.widget.EditText -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import com.iguana.notetaking.databinding.FragmentPdfPageBinding -import dagger.hilt.android.AndroidEntryPoint - - -@AndroidEntryPoint -class PdfPageFragment : Fragment() { - - private val viewModel: PdfViewerViewModel by viewModels() - private var _binding: FragmentPdfPageBinding? = null - private val binding get() = _binding!! - - companion object { - private const val ARG_PDF_URI = "PDF_URI" - private const val ARG_PAGE_INDEX = "PAGE_INDEX" - - fun newInstance(pdfUri: Uri, pageIndex: Int): PdfPageFragment { - val fragment = PdfPageFragment() - val args = Bundle() - args.putString(ARG_PDF_URI, pdfUri.toString()) - args.putInt(ARG_PAGE_INDEX, pageIndex) - fragment.arguments = args - return fragment - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - // XML 레이아웃 파일을 인플레이트하여 반환 - _binding = FragmentPdfPageBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val pdfUriString = arguments?.getString(ARG_PDF_URI) - val pageIndex = arguments?.getInt(ARG_PAGE_INDEX, 0) ?: 0 - - Log.d("PdfPageFragment", "onViewCreated called") // 로그 추가 - - if (pdfUriString != null) { - val pdfUri = Uri.parse(pdfUriString) - val bitmap = viewModel.renderPage(pdfUri, pageIndex) - // binding.pdfImageView.setImageBitmap(bitmap) - - // PhotoView에 이미지 설정 (확대/축소 기능) - binding.photoView.setImageBitmap(bitmap) - } - - } - - - fun addTextBox() { - val editText = EditText(requireContext()).apply { - setText("텍스트") - setBackgroundColor(Color.Transparent.toArgb()) - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - - // 스타일 적용 - setPadding(16, 16, 16, 16) - setTextColor(Color.Black.toArgb()) - textSize = 16f - - // 초기 위치를 중앙으로 설정 - post { - x = (binding.pdfEditorView.width / 2 - width / 2).toFloat() - y = (binding.pdfEditorView.height / 2 - height / 2).toFloat() - } - - // 터치 이벤트로 위치 이동 설정 - var dX = 0f - var dY = 0f - var isDragging = false - - setOnTouchListener { view, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - // 터치 시작 시점의 위치를 기록 - dX = view.x - event.rawX - dY = view.y - event.rawY - isDragging = false - } - - MotionEvent.ACTION_MOVE -> { - // 터치 움직임에 따라 뷰를 이동 - view.animate() - .x(event.rawX + dX) - .y(event.rawY + dY) - .setDuration(0) - .start() - isDragging = true - } - - MotionEvent.ACTION_UP -> { - // 사용자가 드래그하지 않고 클릭만 한 경우 편집 모드로 전환 - if (!isDragging) { - view.performClick() // 클릭 이벤트 수행 - } - } - - else -> return@setOnTouchListener false - } - true - } - - // 클릭 이벤트로 편집 모드 활성화 - setOnClickListener { - this.isFocusableInTouchMode = true - this.requestFocus() - } - } - binding.pdfEditorView.addView(editText) - } - - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfEditor.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfEditor.kt new file mode 100644 index 0000000..5f823d5 --- /dev/null +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfEditor.kt @@ -0,0 +1,89 @@ +package com.iguana.notetaking.pdf + +import android.content.Context +import android.graphics.Color +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +class PdfEditor(private val context: Context) { + + private val _isEditMode = MutableLiveData(false) + val isEditMode: LiveData get() = _isEditMode + + // 편집 모드를 활성화 또는 비활성화 + fun toggleEditMode() { + _isEditMode.value = !(_isEditMode.value ?: false) + } + + // 새로운 텍스트 상자를 PDF 페이지에 추가 + fun addTextBox(parentView: ViewGroup): EditText { + val editText = EditText(context).apply { + setText("텍스트") + setBackgroundColor(Color.TRANSPARENT) + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + + setPadding(16, 16, 16, 16) + setTextColor(Color.BLACK) + textSize = 16f + + // 중앙 위치로 초기화 + post { + x = (parentView.width / 2 - width / 2).toFloat() + y = (parentView.height / 2 - height / 2).toFloat() + } + + // 터치 이벤트로 위치 이동 가능하게 설정 + setDraggable() + } + + // 부모 뷰에 추가 + parentView.addView(editText) + return editText + } + + // 텍스트 상자를 터치로 이동할 수 있도록 설정 + private fun EditText.setDraggable() { + var dX = 0f + var dY = 0f + var isDragging = false + + setOnTouchListener { view, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + dX = view.x - event.rawX + dY = view.y - event.rawY + isDragging = false + } + MotionEvent.ACTION_MOVE -> { + view.animate() + .x(event.rawX + dX) + .y(event.rawY + dY) + .setDuration(0) + .start() + isDragging = true + } + MotionEvent.ACTION_UP -> { + if (!isDragging) { + view.performClick() + } + } + } + true + } + } + + // 텍스트 상자에 포커스를 주고 편집 모드로 진입 + fun enableTextBoxEditing(editText: EditText) { + editText.apply { + isFocusableInTouchMode = true + requestFocus() + } + } +} diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/PdfPageAdapter.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageAdapter.kt similarity index 92% rename from feature/notetaking/src/main/java/com/iguana/notetaking/PdfPageAdapter.kt rename to feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageAdapter.kt index da920f7..95fb2ab 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/PdfPageAdapter.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageAdapter.kt @@ -1,4 +1,4 @@ -package com.iguana.notetaking +package com.iguana.notetaking.pdf import android.net.Uri import androidx.fragment.app.Fragment diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageFragment.kt new file mode 100644 index 0000000..1668427 --- /dev/null +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageFragment.kt @@ -0,0 +1,87 @@ +package com.iguana.notetaking.pdf + +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import com.iguana.notetaking.NotetakingViewModel +import com.iguana.notetaking.databinding.FragmentPdfPageBinding +import dagger.hilt.android.AndroidEntryPoint + + +@AndroidEntryPoint +class PdfPageFragment : Fragment() { + + private val viewModel: PdfViewerViewModel by viewModels() + private val sharedViewModel: NotetakingViewModel by activityViewModels() + private var _binding: FragmentPdfPageBinding? = null + private val binding get() = _binding!! + private lateinit var pdfEditor: PdfEditor + + companion object { + private const val ARG_PDF_URI = "PDF_URI" + private const val ARG_PAGE_INDEX = "PAGE_INDEX" + + fun newInstance(pdfUri: Uri, pageIndex: Int): PdfPageFragment { + val fragment = PdfPageFragment() + val args = Bundle() + args.putString(ARG_PDF_URI, pdfUri.toString()) + args.putInt(ARG_PAGE_INDEX, pageIndex) + fragment.arguments = args + return fragment + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + // XML 레이아웃 파일을 인플레이트하여 반환 + _binding = FragmentPdfPageBinding.inflate(inflater, container, false) + pdfEditor = PdfEditor(requireContext()) // PdfEditor 초기화 + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val pdfUriString = arguments?.getString(ARG_PDF_URI) + val pageIndex = arguments?.getInt(ARG_PAGE_INDEX, 0) ?: 0 + + // 텍스트 모드일 때 + sharedViewModel.isTextMode.observe(viewLifecycleOwner, Observer { isEditMode -> + + }) + + if (pdfUriString != null) { + val pdfUri = Uri.parse(pdfUriString) + val bitmap = viewModel.renderPage(pdfUri, pageIndex) + + // PhotoView에 이미지 설정 (확대/축소 기능) + binding.photoView.setImageBitmap(bitmap) + } + + } + + + // 새로운 텍스트 상자 추가 + private fun addNewTextBox() { + val editText = pdfEditor.addTextBox(binding.pdfEditorView) // pdfEditorView는 PDF 편집용 뷰 그룹 + pdfEditor.enableTextBoxEditing(editText) // 편집 모드 설정 + } + + // 텍스트 편집 완료 후 변경 사항 저장 + private fun saveTextChanges() { + // 텍스트 상자의 위치, 내용 등을 저장하는 로직 구현 + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/PdfViewerFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerFragment.kt similarity index 97% rename from feature/notetaking/src/main/java/com/iguana/notetaking/PdfViewerFragment.kt rename to feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerFragment.kt index 6e678ed..35f57b0 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/PdfViewerFragment.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerFragment.kt @@ -1,14 +1,14 @@ -package com.iguana.notetaking +package com.iguana.notetaking.pdf import android.net.Uri import androidx.fragment.app.viewModels import android.os.Bundle -import android.util.Log import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.viewpager2.widget.ViewPager2 +import com.iguana.notetaking.NotetakingActivity import com.iguana.notetaking.databinding.FragmentPdfViewerBinding import dagger.hilt.android.AndroidEntryPoint diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/PdfViewerViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerViewModel.kt similarity index 83% rename from feature/notetaking/src/main/java/com/iguana/notetaking/PdfViewerViewModel.kt rename to feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerViewModel.kt index dad3386..c8a4bd7 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/PdfViewerViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerViewModel.kt @@ -1,4 +1,4 @@ -package com.iguana.notetaking +package com.iguana.notetaking.pdf import android.graphics.Bitmap import android.net.Uri import android.util.Log @@ -20,6 +20,13 @@ class PdfViewerViewModel @Inject constructor(private val pdfRendererHelper: PdfR private val _currentPageNumber = MutableLiveData() // 페이지 번호를 LiveData로 관리 val currentPageNumber: LiveData get() = _currentPageNumber + private val _isEditMode = MutableLiveData(false) // 텍스트 편집 모드 여부 + val isEditMode: LiveData get() = _isEditMode + + fun toggleEditMode() { + _isEditMode.value = !(_isEditMode.value ?: false) + } + // PDF 파일의 특정 페이지를 렌더링 fun renderPage(uri: Uri, pageIdx: Int): Bitmap? { return pdfRendererHelper.renderPage(uri, pageIdx) From 95e4070b8688f017140e722ec4557cb5b60baeb0 Mon Sep 17 00:00:00 2001 From: aengzu Date: Fri, 1 Nov 2024 19:03:27 +0900 Subject: [PATCH 19/19] =?UTF-8?q?Feat:=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=ED=99=94=EB=A9=B4=EC=97=90=20=EB=9D=84=EC=9A=B0?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=95=EB=8F=84=EB=A1=9C=EB=A7=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *중간에 버그를 잡는 과정에서 코드가 많이 수정된 것 같습니다. *기능 구현이 완료되진 않았지만 리뷰를 위해 PR 보내겠습니다 *현재 텍스트 버튼 누르면 -> 텍스트 편집바가 토글되며 -> 텍스트 편집바가 새로 생기는 경우에는 '주석' 이 추가가 되며 local DB 에 저장하는 부분까지 구현되었습니다 *테스트 정도의 수준으로 DB 저장 시 위치, width, height 는 아직 저장이 안되며, 제출물 제출 후 주말에 작업 예정입니다. *드래그 시 화면 페이지 이동이 같이 되는 버그는 해결했습니다 --- .../iguana/data/local/dao/AnnotationDao.kt | 17 ++--- .../data/local/entity/AnnotationEntity.kt | 4 +- .../iguana/data/mapper/AnnotationMapper.kt | 63 +++++++++++++++---- .../com/iguana/data/mapper/RecordMapper.kt | 4 +- .../repository/AnnotationRepositoryImpl.kt | 50 +++++++++++---- .../com/iguana/domain/model/Annotation.kt | 9 +-- .../domain/repository/AnnotationRepository.kt | 8 ++- .../domain/usecase/ClearAnnotationsUseCase.kt | 12 ++++ .../domain/usecase/LoadAnnotationsUseCase.kt | 16 +++++ .../domain/usecase/SaveAnnotationUseCase.kt | 20 ++++++ .../iguana/notetaking/NotetakingActivity.kt | 15 ++++- .../iguana/notetaking/NotetakingViewModel.kt | 4 ++ .../com/iguana/notetaking/pdf/PdfEditor.kt | 39 +++++------- .../iguana/notetaking/pdf/PdfPageFragment.kt | 44 ++++++++++--- .../iguana/notetaking/pdf/PdfPageViewModel.kt | 42 +++++++++++++ .../notetaking/pdf/PdfViewerFragment.kt | 9 ++- .../notetaking/pdf/PdfViewerViewModel.kt | 20 +++--- 17 files changed, 293 insertions(+), 83 deletions(-) create mode 100644 core/domain/src/main/java/com/iguana/domain/usecase/ClearAnnotationsUseCase.kt create mode 100644 core/domain/src/main/java/com/iguana/domain/usecase/LoadAnnotationsUseCase.kt create mode 100644 core/domain/src/main/java/com/iguana/domain/usecase/SaveAnnotationUseCase.kt create mode 100644 feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageViewModel.kt diff --git a/core/data/src/main/java/com/iguana/data/local/dao/AnnotationDao.kt b/core/data/src/main/java/com/iguana/data/local/dao/AnnotationDao.kt index 6f2351c..a57e20f 100644 --- a/core/data/src/main/java/com/iguana/data/local/dao/AnnotationDao.kt +++ b/core/data/src/main/java/com/iguana/data/local/dao/AnnotationDao.kt @@ -4,22 +4,25 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import com.iguana.data.local.entity.AnnotationEntity @Dao interface AnnotationDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAnnotation(annotation: Annotation) + fun insertAnnotation(annotation: AnnotationEntity) @Query("SELECT * FROM annotations WHERE documentId = :documentId AND pageNumber = :pageNumber") - suspend fun getAnnotationsByPage(documentId: Long, pageNumber: Int): List + fun getAnnotationsByPage(documentId: Long, pageNumber: Int): List @Query("DELETE FROM annotations WHERE id = :id") - suspend fun deleteAnnotation(id: Long) - - @Query("UPDATE annotations SET text = :text, xPosition = :xPosition, yPosition = :yPosition WHERE id = :id") - suspend fun updateAnnotation(id: Long, text: String, xPosition: Float, yPosition: Float) + fun deleteAnnotation(id: Long) + @Query("UPDATE annotations SET content = :content, xPosition = :xPosition, yPosition = :yPosition WHERE id = :id") + fun updateAnnotation(id: Long, content: String, xPosition: Float, yPosition: Float) @Query("DELETE FROM annotations WHERE documentId = :documentId") - suspend fun deleteAnnotationsByDocument(documentId: Long) + fun deleteAnnotationsByDocument(documentId: Long) + + @Query("DELETE FROM annotations") + fun clearAnnotations() } diff --git a/core/data/src/main/java/com/iguana/data/local/entity/AnnotationEntity.kt b/core/data/src/main/java/com/iguana/data/local/entity/AnnotationEntity.kt index b6ac138..99143d7 100644 --- a/core/data/src/main/java/com/iguana/data/local/entity/AnnotationEntity.kt +++ b/core/data/src/main/java/com/iguana/data/local/entity/AnnotationEntity.kt @@ -11,5 +11,7 @@ data class AnnotationEntity( val pageNumber: Int, // 주석이 있는 페이지 번호 val xPosition: Float, // x 좌표 val yPosition: Float, // y 좌표 - val text: String // 주석 텍스트 + val content: String, // 주석 텍스트 + val width: Float, // 너비 + val height: Float // 높이 ) \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/mapper/AnnotationMapper.kt b/core/data/src/main/java/com/iguana/data/mapper/AnnotationMapper.kt index 671a969..f453126 100644 --- a/core/data/src/main/java/com/iguana/data/mapper/AnnotationMapper.kt +++ b/core/data/src/main/java/com/iguana/data/mapper/AnnotationMapper.kt @@ -1,5 +1,6 @@ package com.iguana.data.mapper +import com.iguana.data.local.entity.AnnotationEntity import com.iguana.data.remote.model.* import com.iguana.domain.model.Annotation @@ -7,10 +8,11 @@ import com.iguana.domain.model.Annotation fun AnnotationResponseDto.toDomain() = Annotation( id = id, content = content, - x = x, - y = y, - width = width, - height = height + x = x.toFloat(), + y = y.toFloat(), + width = width.toFloat(), + height = height.toFloat(), + pageNumber = pageNumber ) fun GetAnnotationsResponseDto.toDomain() = annotations.map { it.toDomain() } @@ -18,18 +20,53 @@ fun GetAnnotationsResponseDto.toDomain() = annotations.map { it.toDomain() } fun Annotation.toCreateAnnotationRequestDto(pageNumber: Int): CreateAnnotationRequestDto = CreateAnnotationRequestDto( pageNumber = pageNumber, - x = x, - y = y, - width = width, - height = height, + x = x.toInt(), + y = y.toInt(), + width = width.toInt(), + height = height.toInt(), content = content ) +// 도메인 모델을 로컬 데이터베이스 엔티티로 변환 +fun Annotation.toEntity(documentId:Long, pageNumber:Int) = AnnotationEntity( + id = id ?: 0, // id가 null일 경우 새로 생성된 엔티티로 간주 + documentId = documentId, + pageNumber = pageNumber, + content = content, + xPosition = x, + yPosition = y, + width = width, + height = height +) + fun Annotation.toUpdateAnnotationRequestDto(): UpdateAnnotationRequestDto = UpdateAnnotationRequestDto( content = content, - x = x, - y = y, - width = width, - height = height - ) \ No newline at end of file + x = x.toInt(), + y = y.toInt(), + width = width.toInt(), + height = height.toInt() + ) + +// 로컬 데이터베이스 엔티티를 도메인 모델로 변환 +fun AnnotationEntity.toDomain() = Annotation( + id = id, + content = content, + x = xPosition, + y = yPosition, + width = width, + height = height, + pageNumber = pageNumber +) + +// 서버에서 받은 응답 -> 로컬 데이터베이스 엔티티 +fun AnnotationResponseDto.toEntity() = AnnotationEntity( + id = id, + documentId = documentId, + pageNumber = pageNumber, + content = content, + xPosition = x.toFloat(), + yPosition = y.toFloat(), + width = width.toFloat(), + height = height.toFloat() +) \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt b/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt index 0647926..34d8dce 100644 --- a/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt +++ b/core/data/src/main/java/com/iguana/data/mapper/RecordMapper.kt @@ -71,7 +71,7 @@ fun RecordingFile.toUploadRequestDto(): RecordingUploadRequestDto { // 서버 응답을 기존의 RecordingFile에 덮어씌우는 매퍼 fun RecordingFile.updateWithResponse(response: RecordingUploadResponseDto): RecordingFile { return this.copy( - recordingId = response.recordingId ?: this.recordingId, - documentId = response.documentId ?: this.documentId, + recordingId = response.recordingId, + documentId = response.documentId ) } \ No newline at end of file diff --git a/core/data/src/main/java/com/iguana/data/repository/AnnotationRepositoryImpl.kt b/core/data/src/main/java/com/iguana/data/repository/AnnotationRepositoryImpl.kt index c3729fa..4ab3ba0 100644 --- a/core/data/src/main/java/com/iguana/data/repository/AnnotationRepositoryImpl.kt +++ b/core/data/src/main/java/com/iguana/data/repository/AnnotationRepositoryImpl.kt @@ -1,33 +1,55 @@ package com.iguana.data.repository +import com.iguana.data.local.dao.AnnotationDao import com.iguana.data.remote.api.AnnotationApi import com.iguana.domain.repository.AnnotationRepository import com.iguana.domain.model.Annotation import javax.inject.Inject import com.iguana.data.mapper.toCreateAnnotationRequestDto import com.iguana.data.mapper.toDomain +import com.iguana.data.mapper.toEntity import com.iguana.data.mapper.toUpdateAnnotationRequestDto class AnnotationRepositoryImpl @Inject constructor( - private val annotationApi: AnnotationApi + private val annotationApi: AnnotationApi, + private val annotationDao: AnnotationDao ) : AnnotationRepository { - override suspend fun createAnnotation( + override suspend fun saveAnnotation( documentId: Long, - annotation: Annotation, + annotation: com.iguana.domain.model.Annotation, pageNumber: Int - ): Annotation { - val requestDto = annotation.toCreateAnnotationRequestDto(pageNumber) - val responseDto = annotationApi.createAnnotation(documentId, requestDto) - return responseDto.toDomain() + ) { + // TODO 서버 호출 부분 주석처리 해서 서버 구동되면 주석 해제 + // val requestDto = annotation.toCreateAnnotationRequestDto(pageNumber) + // val responseDto = annotationApi.createAnnotation(documentId, requestDto) + +// val createdAnnotation = responseDto.toDomain() + val annotationEntity = annotation.toEntity(documentId, pageNumber) + + // 로컬에 저장 + annotationDao.insertAnnotation(annotationEntity) + return } - override suspend fun getAnnotations( - documentId: Long, - pageNumbers: List - ): List { + // 특정 페이지 번호로 로컬에서 주석을 조회 + override suspend fun getAnnotationsByPage(documentId: Long, pageNumber: Int): List { + val localAnnotations = annotationDao.getAnnotationsByPage(documentId, pageNumber) + return localAnnotations.map { it.toDomain() } + } + + override suspend fun getAnnotations(documentId: Long, pageNumbers: List): List { + // 서버에서 모든 페이지의 주석을 가져옴 val responseDto = annotationApi.getAnnotations(documentId, pageNumbers) - return responseDto.toDomain() + val remoteAnnotations = responseDto.annotations.map { it.toDomain() } + + // 로컬 데이터베이스에 각 페이지별로 주석 저장 + remoteAnnotations.forEach { annotation -> + val pageNumber = annotation.pageNumber // Annotation에 포함된 페이지 번호 사용 + annotationDao.insertAnnotation(annotation.toEntity(documentId, pageNumber)) + } + + return remoteAnnotations } override suspend fun updateAnnotation(documentId: Long, annotation: Annotation): Annotation { @@ -40,4 +62,8 @@ class AnnotationRepositoryImpl @Inject constructor( annotationApi.deleteAnnotation(documentId, annotationId) } + override suspend fun clearAnnotations() { + annotationDao.clearAnnotations() + } + } \ No newline at end of file diff --git a/core/domain/src/main/java/com/iguana/domain/model/Annotation.kt b/core/domain/src/main/java/com/iguana/domain/model/Annotation.kt index 697586f..c667111 100644 --- a/core/domain/src/main/java/com/iguana/domain/model/Annotation.kt +++ b/core/domain/src/main/java/com/iguana/domain/model/Annotation.kt @@ -2,9 +2,10 @@ package com.iguana.domain.model data class Annotation( val id: Long? = null, // 주석 ID (필요할 지 안할 지는 추후 결정) + val pageNumber: Int, val content: String, // 내용 - val x: Int, // x 좌표 - val y: Int, // y 좌표 - val width: Int, // 너비 - val height: Int // 높이 + val x: Float, // x 좌표 + val y: Float, // y 좌표 + val width: Float, // 너비 + val height: Float // 높이 ) diff --git a/core/domain/src/main/java/com/iguana/domain/repository/AnnotationRepository.kt b/core/domain/src/main/java/com/iguana/domain/repository/AnnotationRepository.kt index e297030..0d9eb03 100644 --- a/core/domain/src/main/java/com/iguana/domain/repository/AnnotationRepository.kt +++ b/core/domain/src/main/java/com/iguana/domain/repository/AnnotationRepository.kt @@ -4,7 +4,10 @@ import com.iguana.domain.model.Annotation interface AnnotationRepository { // 주석 생성 - suspend fun createAnnotation(documentId: Long, annotation: Annotation, pageNumber: Int): Annotation + suspend fun saveAnnotation(documentId: Long, annotation: com.iguana.domain.model.Annotation, pageNumber: Int) + + // 주석 페이지별 조회 + suspend fun getAnnotationsByPage(documentId: Long, pageNumbers: Int): List // 주석 조회 suspend fun getAnnotations(documentId: Long, pageNumbers: List): List @@ -14,4 +17,7 @@ interface AnnotationRepository { // 주석 삭제 suspend fun deleteAnnotation(documentId: Long, annotationId: Long) + + // 테이블 초기화 + suspend fun clearAnnotations() } diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/ClearAnnotationsUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/ClearAnnotationsUseCase.kt new file mode 100644 index 0000000..285ca15 --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/ClearAnnotationsUseCase.kt @@ -0,0 +1,12 @@ +package com.iguana.domain.usecase + +import com.iguana.domain.repository.AnnotationRepository +import javax.inject.Inject + +class ClearAnnotationsUseCase @Inject constructor( + private val annotationRepository: AnnotationRepository +) { + suspend operator fun invoke() { + annotationRepository.clearAnnotations() + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/LoadAnnotationsUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/LoadAnnotationsUseCase.kt new file mode 100644 index 0000000..b2b5f3b --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/LoadAnnotationsUseCase.kt @@ -0,0 +1,16 @@ +package com.iguana.domain.usecase + +import com.iguana.domain.repository.AnnotationRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class LoadAnnotationsUseCase @Inject constructor( + private val annotationRepository: AnnotationRepository +) { + suspend operator fun invoke(documentId: Long, pageNumber: Int): List { + return withContext(Dispatchers.IO) { + annotationRepository.getAnnotationsByPage(documentId, pageNumber) + } + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/iguana/domain/usecase/SaveAnnotationUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/SaveAnnotationUseCase.kt new file mode 100644 index 0000000..9848d4f --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/SaveAnnotationUseCase.kt @@ -0,0 +1,20 @@ +package com.iguana.domain.usecase + +import com.iguana.domain.repository.AnnotationRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class SaveAnnotationUseCase @Inject constructor( + private val annotationRepository: AnnotationRepository +) { + suspend operator fun invoke( + documentId: Long, + annotation: com.iguana.domain.model.Annotation, + pageNumber: Int + ) { + withContext(Dispatchers.IO) { + annotationRepository.saveAnnotation(documentId, annotation, pageNumber) + } + } +} \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt index 08b3f14..2f69e41 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingActivity.kt @@ -4,12 +4,15 @@ import android.Manifest import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.View import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.viewpager2.widget.ViewPager2 import com.iguana.notetaking.databinding.ActivityNotetakingBinding +import com.iguana.notetaking.pdf.PdfPageFragment import com.iguana.notetaking.pdf.PdfViewerFragment import com.iguana.notetaking.sidebar.SideBarFragment import dagger.hilt.android.AndroidEntryPoint @@ -69,7 +72,9 @@ class NotetakingActivity : AppCompatActivity() { binding.toolbar.apply { btnText.setOnClickListener { viewModel.toggleTextMode() - // TODO 주석이 pdf 뷰어에 표시되도록 하는 로직 + if (viewModel.isTextMode.value == true) { + addTextToCurrentPage() + } } btnRecord.setOnClickListener { handleRecordingPermissionAndToggle() } btnAI.setOnClickListener { viewModel.toggleAI() } @@ -102,11 +107,17 @@ class NotetakingActivity : AppCompatActivity() { } - // PDF 뷰어 프래그먼트 가져오기 메서드 private fun getPdfViewerFragment(): PdfViewerFragment? { return supportFragmentManager.findFragmentById(R.id.pdf_fragment_container) as? PdfViewerFragment } + // 현재 페이지에 텍스트 추가 요청을 전달하기 위한 메서드 + private fun addTextToCurrentPage() { + val pdfViewerFragment = getPdfViewerFragment() + val currentPageFragment = pdfViewerFragment?.getCurrentPdfPageFragment() + currentPageFragment?.addNewTextBox(viewModel.pageNumber.value ?: 0) + } + // 사이드바 프래그먼트 가져오기 메서드 private fun getSideBarFragment(): SideBarFragment? { return supportFragmentManager.findFragmentById(R.id.side_bar_container) as? SideBarFragment diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt index c586a74..f2c1bfb 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/NotetakingViewModel.kt @@ -34,9 +34,13 @@ class NotetakingViewModel @Inject constructor() : ViewModel() { // 이전 녹음 상태 추적 변수 private var wasRecording = false + // 텍스트 모드 활성화되어있는지 + private val _isTextMode = MutableLiveData(false) val isTextMode: LiveData get() = _isTextMode + + // 텍스트 모드 토글 함수 fun toggleTextMode() { _isTextMode.value = _isTextMode.value?.not() diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfEditor.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfEditor.kt index 5f823d5..f7d404f 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfEditor.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfEditor.kt @@ -3,21 +3,17 @@ package com.iguana.notetaking.pdf import android.content.Context import android.graphics.Color import android.view.MotionEvent -import android.view.View import android.view.ViewGroup import android.widget.EditText import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import kotlin.math.roundToInt class PdfEditor(private val context: Context) { - private val _isEditMode = MutableLiveData(false) - val isEditMode: LiveData get() = _isEditMode + private val _isDragging = MutableLiveData(false) + val isDragging: LiveData get() = _isDragging - // 편집 모드를 활성화 또는 비활성화 - fun toggleEditMode() { - _isEditMode.value = !(_isEditMode.value ?: false) - } // 새로운 텍스트 상자를 PDF 페이지에 추가 fun addTextBox(parentView: ViewGroup): EditText { @@ -33,44 +29,43 @@ class PdfEditor(private val context: Context) { setTextColor(Color.BLACK) textSize = 16f - // 중앙 위치로 초기화 + // 중앙 위치로 초기화 (Int 단위) post { - x = (parentView.width / 2 - width / 2).toFloat() - y = (parentView.height / 2 - height / 2).toFloat() + x = ((parentView.width / 2 - width / 2)).toFloat() + y = ((parentView.height / 2 - height / 2)).toFloat() } // 터치 이벤트로 위치 이동 가능하게 설정 setDraggable() } - // 부모 뷰에 추가 parentView.addView(editText) return editText } - // 텍스트 상자를 터치로 이동할 수 있도록 설정 + // 텍스트 상자를 터치로 이동할 수 있도록 설정 (Int 단위) private fun EditText.setDraggable() { - var dX = 0f - var dY = 0f - var isDragging = false + var dX = 0 + var dY = 0 setOnTouchListener { view, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { - dX = view.x - event.rawX - dY = view.y - event.rawY - isDragging = false + dX = (view.x - event.rawX).roundToInt() + dY = (view.y - event.rawY).roundToInt() + _isDragging.value = false // 드래그가 시작되지 않음 } MotionEvent.ACTION_MOVE -> { view.animate() - .x(event.rawX + dX) - .y(event.rawY + dY) + .x((event.rawX + dX).roundToInt().toFloat()) + .y((event.rawY + dY).roundToInt().toFloat()) .setDuration(0) .start() - isDragging = true + _isDragging.value = true // 드래그가 시작됨 } MotionEvent.ACTION_UP -> { - if (!isDragging) { + _isDragging.value = false // 드래그 종료 + if (!_isDragging.value!!) { view.performClick() } } diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageFragment.kt index 1668427..0e7e559 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageFragment.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageFragment.kt @@ -2,6 +2,7 @@ package com.iguana.notetaking.pdf import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -9,6 +10,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Observer +import com.iguana.notetaking.NotetakingActivity import com.iguana.notetaking.NotetakingViewModel import com.iguana.notetaking.databinding.FragmentPdfPageBinding import dagger.hilt.android.AndroidEntryPoint @@ -17,7 +19,8 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class PdfPageFragment : Fragment() { - private val viewModel: PdfViewerViewModel by viewModels() + private val pdViewerViewModel: PdfViewerViewModel by viewModels() + private val pdfPageViewModel: PdfPageViewModel by viewModels() private val sharedViewModel: NotetakingViewModel by activityViewModels() private var _binding: FragmentPdfPageBinding? = null private val binding get() = _binding!! @@ -44,6 +47,7 @@ class PdfPageFragment : Fragment() { // XML 레이아웃 파일을 인플레이트하여 반환 _binding = FragmentPdfPageBinding.inflate(inflater, container, false) pdfEditor = PdfEditor(requireContext()) // PdfEditor 초기화 + return binding.root } @@ -53,33 +57,53 @@ class PdfPageFragment : Fragment() { val pdfUriString = arguments?.getString(ARG_PDF_URI) val pageIndex = arguments?.getInt(ARG_PAGE_INDEX, 0) ?: 0 - // 텍스트 모드일 때 - sharedViewModel.isTextMode.observe(viewLifecycleOwner, Observer { isEditMode -> + // 주석을 로드하기 전에 화면에 있던 기존 주석을 제거 + clearAnnotationsOnPage() + + // 현재 페이지 주석 로드 + pdfPageViewModel.loadAnnotations(sharedViewModel.documentId, pageIndex) + + // 드래그 중일 때 ViewPager 스크롤을 비활성화 + pdfEditor.isDragging.observe(viewLifecycleOwner, Observer { isDragging -> + (parentFragment as? PdfViewerFragment)?.setPagingEnabled(!isDragging) }) + if (pdfUriString != null) { val pdfUri = Uri.parse(pdfUriString) - val bitmap = viewModel.renderPage(pdfUri, pageIndex) + val bitmap = pdViewerViewModel.renderPage(pdfUri, pageIndex) // PhotoView에 이미지 설정 (확대/축소 기능) binding.photoView.setImageBitmap(bitmap) } - } // 새로운 텍스트 상자 추가 - private fun addNewTextBox() { - val editText = pdfEditor.addTextBox(binding.pdfEditorView) // pdfEditorView는 PDF 편집용 뷰 그룹 + fun addNewTextBox(pageIndex: Int) { + val editText = pdfEditor.addTextBox(binding.pdfEditorView) pdfEditor.enableTextBoxEditing(editText) // 편집 모드 설정 + // EditText의 위치와 크기 정보를 기반으로 Annotation 객체 생성 + val annotation = com.iguana.domain.model.Annotation( + content = editText.text.toString(), + x = editText.x, + y = editText.y, + width = editText.width.toFloat(), + height = editText.height.toFloat(), + pageNumber = pageIndex + ) + // 주석을 저장하도록 ViewModel 호출 + pdfPageViewModel.saveAnnotation(sharedViewModel.documentId, annotation, pageIndex) } - // 텍스트 편집 완료 후 변경 사항 저장 - private fun saveTextChanges() { - // 텍스트 상자의 위치, 내용 등을 저장하는 로직 구현 + // 주석 제거 + private fun clearAnnotationsOnPage() { + pdfPageViewModel.clearAnnotations() + binding.pdfEditorView.removeAllViews() // pdfEditorView 내 모든 뷰 제거 } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageViewModel.kt new file mode 100644 index 0000000..3c0d7d8 --- /dev/null +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfPageViewModel.kt @@ -0,0 +1,42 @@ +package com.iguana.notetaking.pdf + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.iguana.domain.usecase.LoadAnnotationsUseCase +import com.iguana.domain.usecase.SaveAnnotationUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PdfPageViewModel @Inject constructor( + private val loadAnnotationsUseCase: LoadAnnotationsUseCase, + private val saveAnnotationUseCase: SaveAnnotationUseCase +) : ViewModel() { + private val _annotations = MutableLiveData>() + val annotations: LiveData> get() = _annotations + + fun loadAnnotations(documentId: Long, pageNumber: Int) { + viewModelScope.launch { + val annotations = loadAnnotationsUseCase(documentId, pageNumber) + _annotations.postValue(annotations) + } + } + + fun saveAnnotation( + documentId: Long, + annotation: com.iguana.domain.model.Annotation, + pageNumber: Int + ) { + viewModelScope.launch { + saveAnnotationUseCase(documentId, annotation, pageNumber) + _annotations.value = _annotations.value.orEmpty() + annotation + } + } + + fun clearAnnotations() { + _annotations.value = emptyList() + } +} \ No newline at end of file diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerFragment.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerFragment.kt index 35f57b0..8b5ae12 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerFragment.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerFragment.kt @@ -51,6 +51,9 @@ class PdfViewerFragment : Fragment() { binding.pdfViewPager.adapter = PdfPageAdapter(this, pdfUri, pageCount) } + // 캐시되어있던 다른 document의 주석들 삭제 + viewModel.clearAnnotations() + // ViewPager2의 페이지 변경 리스너 설정 binding.pdfViewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { @@ -59,6 +62,7 @@ class PdfViewerFragment : Fragment() { (activity as? NotetakingActivity)?.onPageChanged(position) } }) + } } @@ -74,7 +78,8 @@ class PdfViewerFragment : Fragment() { _binding = null } - fun getCurrentPage(): Int { - return viewModel.currentPageNumber.value ?: 0 + + fun setPagingEnabled(enabled: Boolean) { + binding.pdfViewPager.isUserInputEnabled = enabled } } diff --git a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerViewModel.kt b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerViewModel.kt index c8a4bd7..62e1e33 100644 --- a/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerViewModel.kt +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/pdf/PdfViewerViewModel.kt @@ -1,4 +1,5 @@ package com.iguana.notetaking.pdf + import android.graphics.Bitmap import android.net.Uri import android.util.Log @@ -6,6 +7,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.iguana.domain.usecase.ClearAnnotationsUseCase import com.iguana.notetaking.util.PdfRendererHelper import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -14,18 +16,15 @@ import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel -class PdfViewerViewModel @Inject constructor(private val pdfRendererHelper: PdfRendererHelper) : +class PdfViewerViewModel @Inject constructor( + private val pdfRendererHelper: PdfRendererHelper, + private val clearAnnotationsUseCase: ClearAnnotationsUseCase +) : ViewModel() { private val _currentPageNumber = MutableLiveData() // 페이지 번호를 LiveData로 관리 val currentPageNumber: LiveData get() = _currentPageNumber - private val _isEditMode = MutableLiveData(false) // 텍스트 편집 모드 여부 - val isEditMode: LiveData get() = _isEditMode - - fun toggleEditMode() { - _isEditMode.value = !(_isEditMode.value ?: false) - } // PDF 파일의 특정 페이지를 렌더링 fun renderPage(uri: Uri, pageIdx: Int): Bitmap? { @@ -47,4 +46,11 @@ class PdfViewerViewModel @Inject constructor(private val pdfRendererHelper: PdfR _currentPageNumber.value = page } + // 캐시되어있던 다른 주석을 모두 삭제하는 메서드 + fun clearAnnotations() { + viewModelScope.launch(Dispatchers.IO) { // IO 디스패처로 실행 + clearAnnotationsUseCase() + } + } + } \ No newline at end of file