From e97cb07a9b4eacdd3c5295b0fe8671bb3ba85215 Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 15 Jul 2025 04:07:23 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore/#72:=20=EB=AA=A8=EB=93=88=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 --- .../main/java/com/paw/key/data/di/RepositoryModule.kt | 9 +++++++++ app/src/main/java/com/paw/key/data/di/ServiceModule.kt | 10 ++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt b/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt index abbd3e91..d2f5908e 100644 --- a/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt +++ b/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt @@ -2,10 +2,12 @@ package com.paw.key.data.di import com.paw.key.data.repositoryimpl.DummyRepositoryImpl import com.paw.key.data.repositoryimpl.RegionRepositoryImpl +import com.paw.key.data.repositoryimpl.WalkCourseRepositoryImpl import com.paw.key.data.repositoryimpl.WalkSharedResultRepositoryImpl import com.paw.key.domain.repository.DummyRepository import com.paw.key.domain.repository.RegionRepository import com.paw.key.domain.repository.WalkSharedResultRepository +import com.paw.key.domain.repository.walkcourse.WalkCourseRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -33,4 +35,11 @@ interface RepositoryModule { fun bindsRegionRepository( regionRepositoryImpl: RegionRepositoryImpl ): RegionRepository + + + @Binds + @Singleton + fun bindsWalkCourseRepository( + walkCourseRepositoryImpl: WalkCourseRepositoryImpl + ): WalkCourseRepository } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/di/ServiceModule.kt b/app/src/main/java/com/paw/key/data/di/ServiceModule.kt index 7fd0552c..a950e2d0 100644 --- a/app/src/main/java/com/paw/key/data/di/ServiceModule.kt +++ b/app/src/main/java/com/paw/key/data/di/ServiceModule.kt @@ -2,11 +2,13 @@ package com.paw.key.data.di import com.paw.key.data.service.DummyService import com.paw.key.data.service.RegionService +import com.paw.key.data.service.walkcourse.WalkCourseService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import retrofit2.Retrofit +import retrofit2.create import javax.inject.Singleton @Module @@ -16,12 +18,16 @@ object ServiceModule { @Provides @Singleton fun providesDummyService(retrofit: Retrofit ): DummyService = - retrofit.create(DummyService::class.java) + retrofit.create() @Provides @Singleton fun providesRegionService(retrofit: Retrofit ): RegionService = - retrofit.create(RegionService::class.java) + retrofit.create() + @Provides + @Singleton + fun providesWalkCourseService(retrofit: Retrofit ): WalkCourseService = + retrofit.create() } \ No newline at end of file From 6df2d7013b2890586e4de103bc52edc73b99f846 Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 15 Jul 2025 04:08:28 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat/#72:=20=EC=82=B0=EC=B1=85=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20api=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../walkcourse/WalkCourseRequestDto.kt | 35 ++++++++++++++++ .../walkcourse/WalkCourseResponseDto.kt | 17 ++++++++ .../remote/datasource/WalkCourseDataSource.kt | 26 ++++++++++++ .../WalkCourseRepositoryImpl.kt | 21 ++++++++++ .../service/walkcourse/WalkCourseService.kt | 20 +++++++++ .../entity/walkcourse/WalkCourseEntity.kt | 41 +++++++++++++++++++ .../walkcourse/WalkCourseRepository.kt | 13 ++++++ 7 files changed, 173 insertions(+) create mode 100644 app/src/main/java/com/paw/key/data/dto/request/walkcourse/WalkCourseRequestDto.kt create mode 100644 app/src/main/java/com/paw/key/data/dto/response/walkcourse/WalkCourseResponseDto.kt create mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/WalkCourseDataSource.kt create mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/WalkCourseRepositoryImpl.kt create mode 100644 app/src/main/java/com/paw/key/data/service/walkcourse/WalkCourseService.kt create mode 100644 app/src/main/java/com/paw/key/domain/model/entity/walkcourse/WalkCourseEntity.kt create mode 100644 app/src/main/java/com/paw/key/domain/repository/walkcourse/WalkCourseRepository.kt diff --git a/app/src/main/java/com/paw/key/data/dto/request/walkcourse/WalkCourseRequestDto.kt b/app/src/main/java/com/paw/key/data/dto/request/walkcourse/WalkCourseRequestDto.kt new file mode 100644 index 00000000..954934c7 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/request/walkcourse/WalkCourseRequestDto.kt @@ -0,0 +1,35 @@ +package com.paw.key.data.dto.request.walkcourse + +import com.paw.key.domain.model.entity.walkcourse.CoordinateEntity +import com.paw.key.domain.model.entity.walkcourse.WalkCourseEntity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CoordinateDto( + val longitude: Double, + val latitude: Double +) + +@Serializable +data class WalkCourseRequestDto( + @SerialName("coordinates") + val coordinates: List, + val distance: Int, + val duration: Int, + val startedAt: String, + val endedAt: String, + val stepCount: Int +) { + fun toEntity(): WalkCourseEntity { + return WalkCourseEntity( + coordinates = coordinates.map { CoordinateEntity(it.longitude, it.latitude) }, + distance = distance, + duration = duration, + startedAt = startedAt, + endedAt = endedAt, + stepCount = stepCount + ) + } +} + diff --git a/app/src/main/java/com/paw/key/data/dto/response/walkcourse/WalkCourseResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/walkcourse/WalkCourseResponseDto.kt new file mode 100644 index 00000000..9e22659f --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/response/walkcourse/WalkCourseResponseDto.kt @@ -0,0 +1,17 @@ +package com.paw.key.data.dto.response.walkcourse + +import com.paw.key.domain.model.entity.walkcourse.WalkCourseRegionIdEntity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class WalkCourseResponseDto( + @SerialName("routeId") + val routeId : Int +) { + fun toEntity(): WalkCourseRegionIdEntity { + return WalkCourseRegionIdEntity( + regionId = routeId + ) + } +} diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/WalkCourseDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/WalkCourseDataSource.kt new file mode 100644 index 00000000..d6756404 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/remote/datasource/WalkCourseDataSource.kt @@ -0,0 +1,26 @@ +package com.paw.key.data.remote.datasource + +import com.paw.key.data.dto.request.walkcourse.WalkCourseRequestDto +import com.paw.key.data.dto.response.BaseResponse +import com.paw.key.data.dto.response.walkcourse.WalkCourseResponseDto +import com.paw.key.data.service.walkcourse.WalkCourseService +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import javax.inject.Inject + +class WalkCourseDataSource @Inject constructor( + private val walkCourseService: WalkCourseService +) { + suspend fun postWalkCourse( + userId: Int, + file: MultipartBody.Part, + walkCourseRequestDto: WalkCourseRequestDto + ): BaseResponse { + val jsonString = Json.encodeToString(WalkCourseRequestDto.serializer(), walkCourseRequestDto) + val requestBody = jsonString.toRequestBody("application/json".toMediaType()) + + return walkCourseService.postWalkCourse(userId, file, requestBody) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/WalkCourseRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/WalkCourseRepositoryImpl.kt new file mode 100644 index 00000000..8e3b1f65 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/WalkCourseRepositoryImpl.kt @@ -0,0 +1,21 @@ +package com.paw.key.data.repositoryimpl + +import com.paw.key.data.dto.request.walkcourse.WalkCourseRequestDto +import com.paw.key.data.remote.datasource.WalkCourseDataSource +import com.paw.key.domain.model.entity.walkcourse.WalkCourseRegionIdEntity +import com.paw.key.domain.repository.walkcourse.WalkCourseRepository +import okhttp3.MultipartBody +import javax.inject.Inject + +class WalkCourseRepositoryImpl @Inject constructor( + private val walkCourseDataSource: WalkCourseDataSource +) : WalkCourseRepository { + override suspend fun postWalkCourse( + userId: Int, + image: MultipartBody.Part, + routeRequestDto: WalkCourseRequestDto + ): Result = runCatching { + walkCourseDataSource.postWalkCourse(userId, image, routeRequestDto) + .data.toEntity() + } +} diff --git a/app/src/main/java/com/paw/key/data/service/walkcourse/WalkCourseService.kt b/app/src/main/java/com/paw/key/data/service/walkcourse/WalkCourseService.kt new file mode 100644 index 00000000..47b137aa --- /dev/null +++ b/app/src/main/java/com/paw/key/data/service/walkcourse/WalkCourseService.kt @@ -0,0 +1,20 @@ +package com.paw.key.data.service.walkcourse + +import com.paw.key.data.dto.response.BaseResponse +import com.paw.key.data.dto.response.walkcourse.WalkCourseResponseDto +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +interface WalkCourseService { + @Multipart + @POST("routes") + suspend fun postWalkCourse( + @Header("X-USER-ID") userId: Int, + @Part trackingImage: MultipartBody.Part, + @Part("routeRequest") routeRequest: RequestBody + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/model/entity/walkcourse/WalkCourseEntity.kt b/app/src/main/java/com/paw/key/domain/model/entity/walkcourse/WalkCourseEntity.kt new file mode 100644 index 00000000..b96e201b --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/model/entity/walkcourse/WalkCourseEntity.kt @@ -0,0 +1,41 @@ +package com.paw.key.domain.model.entity.walkcourse + +import com.paw.key.data.dto.request.walkcourse.CoordinateDto +import com.paw.key.data.dto.request.walkcourse.WalkCourseRequestDto + +data class WalkCourseEntity ( + val coordinates: List, + val distance: Int, + val duration: Int, + val startedAt: String, + val endedAt: String, + val stepCount: Int +) { + fun toDto(): WalkCourseRequestDto { + return WalkCourseRequestDto( + coordinates = coordinates.map { it.toDto() }, + distance = distance, + duration = duration, + startedAt = startedAt, + endedAt = endedAt, + stepCount = stepCount + ) + } +} + +data class CoordinateEntity( + val latitude: Double, + val longitude: Double +) { + fun toDto(): CoordinateDto { + return CoordinateDto( + latitude = latitude, + longitude = longitude + ) + } +} + +data class WalkCourseRegionIdEntity( + val regionId: Int +) + diff --git a/app/src/main/java/com/paw/key/domain/repository/walkcourse/WalkCourseRepository.kt b/app/src/main/java/com/paw/key/domain/repository/walkcourse/WalkCourseRepository.kt new file mode 100644 index 00000000..03e786eb --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/repository/walkcourse/WalkCourseRepository.kt @@ -0,0 +1,13 @@ +package com.paw.key.domain.repository.walkcourse + +import com.paw.key.data.dto.request.walkcourse.WalkCourseRequestDto +import com.paw.key.domain.model.entity.walkcourse.WalkCourseRegionIdEntity +import okhttp3.MultipartBody + +interface WalkCourseRepository { + suspend fun postWalkCourse( + userId: Int, + image: MultipartBody.Part, + routeRequestDto: WalkCourseRequestDto + ): Result +} \ No newline at end of file From 702356895201865ab04bc4d95decfeec2f7dcf0c Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 15 Jul 2025 04:10:00 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat/#72:=20=EC=82=B0=EC=B1=85=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EA=B0=80=EC=A0=B8=EC=99=80=EC=84=9C=20UI=EC=97=90?= =?UTF-8?q?=20=EB=B0=98=EC=98=81=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/paw/key/core/util/PhotoUtils.kt | 98 +++++++++++++++++++ .../ui/course/walk/WalkCourseScreen.kt | 61 +++++------- .../course/walk/state/WalkCourseContract.kt | 3 + .../walk/viewmodel/WalkCourseViewModel.kt | 65 +++++++++++- 4 files changed, 191 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/paw/key/core/util/PhotoUtils.kt diff --git a/app/src/main/java/com/paw/key/core/util/PhotoUtils.kt b/app/src/main/java/com/paw/key/core/util/PhotoUtils.kt new file mode 100644 index 00000000..ea9a0a19 --- /dev/null +++ b/app/src/main/java/com/paw/key/core/util/PhotoUtils.kt @@ -0,0 +1,98 @@ +package com.paw.key.core.util + +import android.graphics.Bitmap +import android.util.Log +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.ByteArrayOutputStream + +class PhotoUtils { + companion object { + private const val MAX_WIDTH = 800 + private const val MAX_HEIGHT = 600 + private const val DEFAULT_QUALITY = 80 + + private fun resizeBitmapIfNeeded(bitmap: Bitmap): Bitmap { + val width = bitmap.width + val height = bitmap.height + + // 이미 적절한 크기인 경우 원본 반환 + if (width <= MAX_WIDTH && height <= MAX_HEIGHT) { + return bitmap + } + + // 비율 계산 + val aspectRatio = width.toFloat() / height.toFloat() + val (newWidth, newHeight) = if (aspectRatio > 1) { + // 가로가 더 긴 경우 + val calculatedHeight = (MAX_WIDTH / aspectRatio).toInt() + MAX_WIDTH to minOf(calculatedHeight, MAX_HEIGHT) + } else { + // 세로가 더 긴 경우 + val calculatedWidth = (MAX_HEIGHT * aspectRatio).toInt() + minOf(calculatedWidth, MAX_WIDTH) to MAX_HEIGHT + } + + return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) + } + + fun createBitmapMultipart( + bitmap: Bitmap, + partName: String = "image", + format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, + quality: Int = DEFAULT_QUALITY + ): MultipartBody.Part? { + return try { + val bos = ByteArrayOutputStream() + + // Bitmap 최적화 + val optimizedBitmap = resizeBitmapIfNeeded(bitmap) + + // 압축 + val compressedSuccessfully = optimizedBitmap.compress(format, quality, bos) + if (!compressedSuccessfully) { + Log.e("PhotoUtils", "Bitmap compression failed") + return null + } + + val byteArray = bos.toByteArray() + if (byteArray.isEmpty()) { + Log.e("PhotoUtils", "Compressed image data is empty") + return null + } + + // MIME 타입 결정 + val mimeType = when (format) { + Bitmap.CompressFormat.PNG -> "image/png" + else -> "image/jpeg" + } + + // 파일 확장자 결정 + val extension = when (format) { + Bitmap.CompressFormat.PNG -> "png" + else -> "jpg" + } + + val requestBody = byteArray.toRequestBody(mimeType.toMediaTypeOrNull()) + + // 원본과 다른 경우에만 리사이클 + if (optimizedBitmap != bitmap) { + optimizedBitmap.recycle() + } + + bos.close() + + MultipartBody.Part.createFormData( + name = partName, + filename = "image_${System.currentTimeMillis()}.${extension}", + body = requestBody + ) + + } catch (e: Exception) { + Log.e("PhotoUtils", "createBitmapMultipart - ${e.message}") + null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt index df46a541..db6e3e6f 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -92,6 +91,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import java.nio.IntBuffer +import java.time.LocalDateTime import java.util.Locale import java.util.concurrent.TimeUnit import javax.microedition.khronos.egl.EGL10 @@ -202,7 +202,8 @@ fun WalkCourseRoute( copy( isRecording = true, currentLocation = currentLocation, - initialLocationState = UiState.Success(currentLocation) + initialLocationState = UiState.Success(currentLocation), + startedAt = LocalDateTime.now().toString(), ) } @@ -329,15 +330,13 @@ fun WalkCourseRoute( // 캡처 부분 LaunchedEffect(state.shouldCaptureMap, state.poiPoints) { - delay(500) - if (state.shouldCaptureMap) { val glSurfaceView = mapView.surfaceView as? GLSurfaceView if (glSurfaceView != null) { withContext(Dispatchers.IO) { captureMapToBitmap(glSurfaceView) { capturedBitmap -> capturedBitmap?.let { - viewModel.onMapCaptured(it) + viewModel.onMapCaptured(it) // 캡처된 비트맵을 ViewModel로 전달 Log.d("WalkCourseRoute", "맵 캡처 성공! (triggered by shouldCaptureMap)") } ?: run { Log.e("WalkCourseRoute", "맵 캡처 실패: 비트맵이 null입니다.") @@ -375,7 +374,7 @@ fun WalkCourseRoute( viewModel.updateState { copy( isRecording = !this.isRecording, - shouldCaptureMap = true + shouldCaptureMap = true, ) } }, @@ -389,9 +388,12 @@ fun WalkCourseRoute( onStopTracking = { viewModel.updateState { copy( - isLocationTracking = !this.isLocationTracking + isLocationTracking = !this.isLocationTracking, + endedAt = LocalDateTime.now().toString() ) } + + viewModel.postWalkCourseData(2) }, onCaptured = { bitmap -> viewModel.onMapCaptured(bitmap) @@ -466,11 +468,12 @@ fun WalkCourseScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { + // Todo : 텍스트 스타일 24b로 변경 예쩡 if (!isSharedWalk) { Text( text = "산책이 중단되었어요!", textAlign = TextAlign.Center, - style = PawKeyTheme.typography.head24B, + style = PawKeyTheme.typography.head22B, color = PawKeyTheme.colors.white1, modifier = Modifier.fillMaxWidth() ) @@ -481,9 +484,7 @@ fun WalkCourseScreen( textAlign = TextAlign.Center, style = PawKeyTheme.typography.body16M, color = PawKeyTheme.colors.white2, - modifier = Modifier - .padding(top = 12.dp) - .fillMaxWidth() + modifier = Modifier.fillMaxWidth() ) } else { Text( @@ -513,28 +514,28 @@ fun WalkCourseScreen( .navigationBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally ) { - if (isTracking) { - Row ( - modifier = Modifier - .fillMaxWidth() - ) { - Spacer(modifier = Modifier.weight(1f)) + Row ( + modifier = Modifier + .fillMaxWidth() + ) { + Spacer(modifier = Modifier.weight(1f)) + if (isTracking) { FloatingActionButton( shape = CircleShape, onClick = onClickTracking, - containerColor = PawKeyTheme.colors.white1, - modifier = Modifier - .size(44.dp) + containerColor = Color.White ) { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_course_map_tap_location_on), contentDescription = "내 위치",//stringResource(id = R.string.lo) - tint = Color.Unspecified + tint = Color.Black ) } } + } + if (isTracking) { PawkeyButton( text = "중지하기", enabled = true, @@ -559,14 +560,13 @@ fun WalkCourseScreen( }, modifier = Modifier .padding(top = 16.dp) - .padding(bottom = 44.dp) ) } else { Row ( modifier = Modifier .fillMaxWidth() .padding(16.dp) - ) { + ){ Text( text = "계속 산책하기", modifier = Modifier @@ -583,7 +583,7 @@ fun WalkCourseScreen( color = PawKeyTheme.colors.green500, shape = RoundedCornerShape(8.dp) ) - .padding(horizontal = 28.dp, vertical = 16.dp), + .padding(horizontal = 24.dp, vertical = 16.dp), color = PawKeyTheme.colors.green500, style = PawKeyTheme.typography.body16Sb ) @@ -602,7 +602,7 @@ fun WalkCourseScreen( navigateNext() onStopTracking() } - .padding(horizontal = 28.dp, vertical = 16.dp), + .padding(horizontal = 24.dp, vertical = 16.dp), color = PawKeyTheme.colors.white1, style = PawKeyTheme.typography.body16Sb ) @@ -618,18 +618,13 @@ fun captureMapToBitmap(surfaceView: GLSurfaceView, onCaptured: (Bitmap?) -> Unit surfaceView.queueEvent { val egl = EGLContext.getEGL() as EGL10 val gl = egl.eglGetCurrentContext().gl as GL10 + val bitmap = createBitmapFromGLSurface(0, 0, surfaceView.width, surfaceView.height, gl) - // 원하는 최종 크기를 먼저 계산 - val screenWidth = surfaceView.context.resources.displayMetrics.widthPixels - val contentWidth = (screenWidth - 32) - val targetHeight = (156 * surfaceView.context.resources.displayMetrics.density).toInt() - - val bitmap = createBitmapFromGLSurface(0, 0, surfaceView.width, surfaceView.height, gl, contentWidth, targetHeight) onCaptured(bitmap) } } -fun createBitmapFromGLSurface(x: Int, y: Int, w: Int, h: Int, gl: GL10, targetWidth: Int, targetHeight: Int): Bitmap? { +fun createBitmapFromGLSurface(x: Int, y: Int, w: Int, h: Int, gl: GL10): Bitmap? { val bitmapBuffer = IntArray(w * h) val bitmapSource = IntArray(w * h) val intBuffer = IntBuffer.wrap(bitmapBuffer) @@ -666,10 +661,8 @@ fun createBitmapFromGLSurface(x: Int, y: Int, w: Int, h: Int, gl: GL10, targetWi var cropWidth: Int var cropHeight: Int - // 비율 조정 val currentAspectRatio = w.toFloat() / h.toFloat() - // 가로와 세로의 비율 조정 - 가로가 크다면 세로를 증가, 세로가 크다면 가로로 증가 if (currentAspectRatio > targetAspectRatio) { cropHeight = h cropWidth = (h * targetAspectRatio).toInt() diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt index c60fd025..04a1ee79 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt @@ -15,6 +15,9 @@ class WalkCourseContract { val uiState: UiState> = UiState.Loading, val poiPoints: PersistentList = persistentListOf(), + val startedAt: String = "", + val endedAt: String = "", + val bitmap: Bitmap? = null, // 현재 걸음 수 diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt index 05f5cb2c..323fff04 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt @@ -7,8 +7,12 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kakao.vectormap.LatLng +import com.paw.key.core.util.PhotoUtils import com.paw.key.core.util.PreferenceDataStore +import com.paw.key.domain.model.entity.walkcourse.CoordinateEntity +import com.paw.key.domain.model.entity.walkcourse.WalkCourseEntity import com.paw.key.domain.repository.WalkSharedResultRepository +import com.paw.key.domain.repository.walkcourse.WalkCourseRepository import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseSideEffect import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseState import dagger.hilt.android.lifecycle.HiltViewModel @@ -21,12 +25,19 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream import javax.inject.Inject @HiltViewModel class WalkCourseViewModel @Inject constructor( @ApplicationContext private val context: Context, private val walkSharedResultRepository : WalkSharedResultRepository, + private val walkCourseRepository: WalkCourseRepository ) : ViewModel() { private val _state = MutableStateFlow(WalkCourseState()) val state : StateFlow @@ -53,9 +64,60 @@ class WalkCourseViewModel @Inject constructor( ) } + // 서버 통신 + fun postWalkCourseData(userId: Int) = viewModelScope.launch { + val bitmap = state.value.bitmap + if (bitmap == null) { + _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 이미지가 없습니다.")) + return@launch + } + + try { + // PhotoUtils 사용 + val imagePart = PhotoUtils.createBitmapMultipart( + bitmap = bitmap, + partName = "trackingImage" + ) + + if (imagePart == null) { + _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("이미지 변환 실패")) + return@launch + } + + val routeEntity = WalkCourseEntity( + coordinates = state.value.poiPoints.map { // la, lo + CoordinateEntity(it.longitude, it.latitude) + }, + distance = state.value.totalDistance.toInt(), + duration = (_totalTime.value).toInt(), + startedAt = state.value.startedAt, + endedAt = state.value.endedAt, + stepCount = state.value.steps.toInt() + ) + + val result = walkCourseRepository.postWalkCourse( + userId = userId, + image = imagePart, + routeRequestDto = routeEntity.toDto() + ) + + result.onSuccess { response -> + _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("루트 업로드 완료: routeId=${response}")) + Log.d("WalkCourseViewModel", "routeId = ${response}") + }.onFailure { throwable -> + _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("업로드 실패: ${throwable.message}")) + Log.e("WalkCourseViewModel", "업로드 실패", throwable) + } + + } catch (e: Exception) { + _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("오류 발생: ${e.localizedMessage}")) + Log.e("WalkCourseViewModel", "예외 발생", e) + } + } + + // Todo : = updateState 로 일관되게 정리하기 fun updateLocationAndCalculateDistance(newLocation: LatLng, accuracy: Float) { - // GPS 정확도가 너무 낮은 경우 (예: 10m 이상) 무시 val MIN_ACCURACY_THRESHOLD = 25f // 미터 단위 (이보다 높은 정확도일 때만 사용) if (accuracy > MIN_ACCURACY_THRESHOLD) { return @@ -198,7 +260,6 @@ class WalkCourseViewModel @Inject constructor( steps = currentWalkState.steps.toInt(), points = currentWalkState.poiPoints.toList() ) - Log.d("WalkCourseViewModel", "All walk summary data saved successfully using PreferenceDataStore.") Log.e("WalkCourseViewModel", PreferenceDataStore.getTotalTime(context).toString()) _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록이 성공적으로 저장되었습니다.")) } catch (e: Exception) { From dd2abcf5da2354cd812aaaaaefd2bcecd7bdac3d Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 15 Jul 2025 04:10:17 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat/#72:=20=EB=92=A4=EB=A1=9C=20=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=A7=89=EA=B8=B0=20-=20=EC=82=B0=EC=B1=85=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=ED=9B=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/course/walkcomplete/WalkCompletionScreen.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/WalkCompletionScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/WalkCompletionScreen.kt index 975e63af..8ca4b85e 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/WalkCompletionScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/WalkCompletionScreen.kt @@ -2,28 +2,24 @@ package com.paw.key.presentation.ui.course.walkcomplete import android.graphics.Bitmap import android.util.Log +import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -32,8 +28,6 @@ import com.paw.key.R import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.presentation.ui.course.walk.component.WalkRecordRow import com.paw.key.presentation.ui.course.walk.formatDistance import com.paw.key.presentation.ui.course.walk.formatTime import com.paw.key.presentation.ui.course.walkcomplete.component.WalkCompleteHeader @@ -53,6 +47,11 @@ fun WalkCompletionRoute( val walkRecordList = listOf(R.string.course_record_distance, R.string.course_record_time, R.string.course_record_step) + BackHandler(enabled = true) { + // 뒤로 가기 막기 + } + + LaunchedEffect(Unit) { viewModel.loadWalkResult() // 디버깅용 @@ -95,6 +94,7 @@ fun WalkCompletionScreen( title = "산책 완료", onBackClick = navigateUp, modifier = Modifier + .padding(8.dp) .background(PawKeyTheme.colors.white1), isBackVisible = false ) From 3de3ef81bae93156bac483c28e733b0a80096f6e Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 15 Jul 2025 04:10:53 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore/#72:=20tap=20map=20view=20contract=20?= =?UTF-8?q?class=20wrap=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entire/tab/map/state/TapMapContract.kt | 25 ++++++++----------- .../tab/map/viewmodel/TapMapViewModel.kt | 5 ++-- .../key/presentation/ui/login/LoginScreen.kt | 1 - 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt index 3241cf7a..d205012e 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt @@ -4,19 +4,16 @@ import androidx.compose.runtime.Immutable import com.kakao.vectormap.LatLng import com.paw.key.core.util.UiState -class TapMapContract { - @Immutable - data class TapMapState( - val initialLocationState : UiState = UiState.Loading, - val currentLocation: LatLng? = null, - val isLocationTracking: Boolean = false, - val isTrackingEnabled: Boolean = false, - ) - - sealed class TapMapSideEffect { - data class ShowSnackBar(val message: String) : TapMapSideEffect() - data object NavigateUp: TapMapSideEffect() - data object NavigateNext: TapMapSideEffect() - } +@Immutable +data class TapMapState( + val initialLocationState : UiState = UiState.Loading, + val currentLocation: LatLng? = null, + val isLocationTracking: Boolean = false, + val isTrackingEnabled: Boolean = false, +) +sealed class TapMapSideEffect { + data class ShowSnackBar(val message: String) : TapMapSideEffect() + data object NavigateUp: TapMapSideEffect() + data object NavigateNext: TapMapSideEffect() } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt index 64e78834..2f74d20c 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt @@ -3,9 +3,8 @@ package com.paw.key.presentation.ui.course.entire.tab.map.viewmodel import androidx.lifecycle.ViewModel import com.kakao.vectormap.LatLng import com.paw.key.core.util.UiState -import com.paw.key.presentation.ui.course.entire.tab.map.state.TapMapContract -import com.paw.key.presentation.ui.course.entire.tab.map.state.TapMapContract.TapMapSideEffect -import com.paw.key.presentation.ui.course.entire.tab.map.state.TapMapContract.TapMapState +import com.paw.key.presentation.ui.course.entire.tab.map.state.TapMapSideEffect +import com.paw.key.presentation.ui.course.entire.tab.map.state.TapMapState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt index 063bc10c..1f5d8055 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt @@ -30,7 +30,6 @@ import com.paw.key.R import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.isKeyboardOpen import com.paw.key.core.util.noRippleClickable import com.paw.key.presentation.ui.login.component.LoginTextField import com.paw.key.presentation.ui.login.viewmodel.LoginViewModel