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 6e352f3..73a9b08 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -25,11 +25,11 @@ 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 에 있는 걸 구현하므로 + 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 b4477cc..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 @@ -14,6 +14,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 +49,12 @@ abstract class DataModule { fun provideRecentFileDao(appDatabase: AppDatabase): RecentFileDao { return appDatabase.recentFileDao() } + + @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/files/RecordingFileStorage.kt b/core/data/src/main/java/com/iguana/data/local/files/RecordingFileStorage.kt index ac88ecf..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 @@ -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 ) { // 로컬 스토리지에 녹음 파일 저장 @@ -16,16 +16,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) @@ -39,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 c0a8a9c..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 @@ -15,24 +13,41 @@ 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 + ) + } ) } +// 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( - prevPage = event.pageNumber - 1, - nextPage = event.pageNumber, + prevPage = event.prevPage, + nextPage = event.nextPage, timestamp = event.timestamp.toDouble() ) } } - // 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/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..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 @@ -1,6 +1,8 @@ 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 @@ -24,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.documentName.toLong(), 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 } } @@ -47,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 @@ -71,11 +81,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() ) // 페이지 이동 이벤트 로컬에 저장 } } @@ -86,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/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..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,15 +11,18 @@ interface RecordRepository { suspend fun saveRecordingFile(recordingFile: RecordingFile) // 로컬에 있는 녹음 파일 삭제 - suspend fun deleteRecordingFile(recordingFile: RecordingFile) + suspend fun deleteRecordingFile(filePath: String) // 페이지 이동 이벤트 업로드 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) + // 로컬에서 모든 페이지 이동 이벤트를 로드 + 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/SavePageTurnEventUseCase.kt b/core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventUseCase.kt new file mode 100644 index 0000000..15dcc57 --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/SavePageTurnEventUseCase.kt @@ -0,0 +1,24 @@ +package com.iguana.domain.usecase +import com.iguana.domain.model.record.PageTurnEvent +import com.iguana.domain.repository.RecordRepository +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/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..24f5bce --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/UploadPageTurnEventsUseCase.kt @@ -0,0 +1,14 @@ +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) { + 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 new file mode 100644 index 0000000..8620c35 --- /dev/null +++ b/core/domain/src/main/java/com/iguana/domain/usecase/UploadRecordingUseCase.kt @@ -0,0 +1,40 @@ +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 +import javax.inject.Inject + +class UploadRecordingUseCase @Inject constructor( + private val recordRepository: RecordRepository +) { + suspend operator fun invoke( + documentId: Long, + filePath: String, + fileName: String + ): Long { + val recordingFile = createRecordingFile(documentId, filePath, fileName) + val uploadedRecording = recordRepository.uploadRecordingFile(recordingFile) + + return uploadedRecording.recordingId ?: throw Exception("recordingId가 없습니다") + } + + // 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/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/AndroidManifest.xml b/feature/notetaking/src/main/AndroidManifest.xml index 730dd70..a575235 100644 --- a/feature/notetaking/src/main/AndroidManifest.xml +++ b/feature/notetaking/src/main/AndroidManifest.xml @@ -5,15 +5,14 @@ - - - + + + \ 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 139c7ef..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,18 +1,33 @@ 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() { + + 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 +39,141 @@ 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() + observeToolbar() + } + + // 뷰 초기화 메서드 + 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) + + setupTitleBar() + setupToolbar() + setupPdfViewerAndSidebar() + } + + // 페이지 변경 시 호출되는 메서드 + fun onPageChanged(pageNumber: Int) { + viewModel.setPageNumber(pageNumber) + updateSidebarWithPage(pageNumber) + } + + // 툴바 설정 + 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.titleBar.titleBar.text = pdfTitle - // 뒤로가기 버튼 클릭 리스너 설정 + // 타이틀바 설정 + private fun setupTitleBar() { binding.titleBar.backButton.setOnClickListener { finish() } + binding.titleBar.titleBar.text = viewModel.pdfTitle + } - // PDF URI가 있는 경우 프래그먼트 추가 - if (pdfUriString != null) { - val pdfUri = Uri.parse(pdfUriString) - val pdfViewerFragment = PdfViewerFragment.newInstance(pdfUri) + // 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) + ) + } - supportFragmentManager.beginTransaction() - .replace(R.id.pdf_fragment_container, pdfViewerFragment) - .commit() + // 프래그먼트 교체 메서드 + private fun replaceFragment(containerId: Int, fragment: androidx.fragment.app.Fragment) { + supportFragmentManager.beginTransaction() + .replace(containerId, fragment) + .commit() + } - val sideBarFragment = SideBarFragment.newInstance(viewModel.documentId, viewModel.pageNumber.value ?: 0) - supportFragmentManager.beginTransaction() - .replace(R.id.side_bar_container, sideBarFragment) - .commit() + // PDF 에러 처리 메서드 + private fun handlePdfError(message: String) { + Log.e("NotetakingActivity", message) + Toast.makeText(this, "PDF 파일을 열 수 없습니다.", Toast.LENGTH_SHORT).show() + } - // 툴바의 텍스트 버튼 클릭 리스너 설정 - binding.toolbar.btnText.setOnClickListener { - pdfViewerFragment.getCurrentPdfPageFragment()?.addTextBox() + // 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) + } + + // 뷰모델의 상태를 관찰하여 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() } + } + + // AI 탭의 활성화 상태 관찰 + viewModel.isAIActive.observe(this) { isActive -> + 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.e("NotetakingActivity", "PDF URI is null in Activity") // URI가 null인 경우 로그 + // 이미 권한이 있는 경우 + Log.d("NotetakingActivity", "이미 녹음 권한이 있습니다.") } } - // 페이지 변경 시 호출되는 메서드 - fun onPageChanged(pageNumber: Int) { - viewModel.setPageNumber(pageNumber) - // 사이드바 프래그먼트 해당 페이지 내용으로 업데이트 - val sideBarFragment = supportFragmentManager.findFragmentById(R.id.side_bar_container) as? SideBarFragment - sideBarFragment?.updatePageNumber(pageNumber) + + // 권한 요청 결과 처리 + 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에 녹음 중지 요청 } -} \ 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..2b4a8e4 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,40 @@ 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 + + // 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 } @@ -26,4 +46,31 @@ class NotetakingViewModel @Inject constructor( 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!! + } } 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..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 @@ -1,14 +1,18 @@ 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 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 @@ -50,10 +54,11 @@ class RecordFragment() : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + observeRecordingState() } - // 페이지 번호 업데이트 메서드 + // 페이지 번호 업데이트 메서드 -> 페이지 이동 이벤트 발생시 상위 프래그먼트에서 호출되는 함수 fun updateContentForPage(pageNumber: Int) { if (isAdded && !isDetached) { // Fragment가 활성 상태인지 확인 viewModel.setPageNumber(pageNumber+1) @@ -65,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..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,24 +1,146 @@ 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 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 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 + handle: SavedStateHandle, + private val uploadRecordingUseCase: UploadRecordingUseCase, + private val uploadPageTurnEventsUseCase: UploadPageTurnEventsUseCase, + 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 + + + private val _recordingStatus = MutableLiveData() + val recordingStatus: LiveData get() = _recordingStatus + + private var startTimeMillis: Long = 0L - private val _pageNumber = MutableLiveData() + 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 + if (isRecording()) { + viewModelScope.launch { + savePageTurnEvent(pageNumber) + } + prevPage = pageNumber + } else { + Log.d("RecordViewModel", "녹음 상태가 아님, 저장하지 않음") + } + } + + fun startRecording(context: Context) { + try { + val intent = Intent(context, RecordingService::class.java).apply { + action = ACTION_START_RECORDING + } + context.startService(intent) + _recordingStatus.value = true // 녹음 시작 전에 상태를 true로 설정 + startTimeMillis = System.currentTimeMillis() + } catch (e: Exception) { + Log.e("RecordViewModel", "녹음 시작 실패: ${e.message}") + } + } + + fun stopRecording(context: Context) { + sendStopIntent(context) + _recordingStatus.value = false + } + + private fun sendStopIntent(context: Context) { + val intent = Intent(context, RecordingService::class.java).apply { + action = ACTION_STOP_RECORDING + } + context.startService(intent) + } + + 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 new file mode 100644 index 0000000..f735eb6 --- /dev/null +++ b/feature/notetaking/src/main/java/com/iguana/notetaking/recording/RecordingService.kt @@ -0,0 +1,148 @@ +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 + private var isRecording = false + private val channelId = "RecordingServiceChannel" + private val notificationId = 1 + private var outputFilePath: String? = null + + 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 + } + + // 권한 확인 + if (ContextCompat.checkSelfPermission( + this, Manifest.permission.RECORD_AUDIO + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.e("RecordingService", "녹음 권한이 없습니다.") + return + } + + outputFilePath = getRecordingFilePath(this) + if (outputFilePath == null) { + Log.e("RecordingService", "녹음 파일 경로를 생성할 수 없습니다.") + return + } + + recorder = MediaRecorder().apply { + try { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) + setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) + setOutputFile(outputFilePath) + + prepare() + start() + isRecording = true + startForeground(notificationId, createRecordingNotification("녹음 중...")) + } catch (e: IllegalStateException) { + Log.e("RecordingService", "설정 오류: ${e.message}") + releaseRecorder() + } catch (e: IOException) { + Log.e("RecordingService", "prepare() 실패: ${e.message}") + releaseRecorder() + } + } + } + + private fun releaseRecorder() { + recorder?.release() + recorder = null + isRecording = false + } + + private fun stopRecording() { + if (!isRecording) { + Log.w("RecordingService", "현재 녹음이 진행 중이지 않습니다.") + return + } + + recorder?.apply { + stop() + release() + } + recorder = null + isRecording = false + stopForeground(true) + sendRecordingFinishedBroadcast() + stopSelf() + } + + 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 + } + + private fun getRecordingFilePath(context: Context): String { + // 외부 저장소의 앱 전용 디렉토리 + val directory = context.getExternalFilesDir(null) + 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) + } + } + +} 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-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/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/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..401fb6e 100644 --- a/feature/notetaking/src/main/res/values/strings.xml +++ b/feature/notetaking/src/main/res/values/strings.xml @@ -11,4 +11,7 @@ AI 요약이 실패했습니다. 상태를 확인할 수 없습니다. 요약 데이터가 없습니다. + 무제 + 녹음 중이 아닙니다. + 녹음 중.. \ No newline at end of file