Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions app/src/main/java/com/paw/key/core/util/PhotoUtils.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/com/paw/key/data/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -33,4 +35,11 @@ interface RepositoryModule {
fun bindsRegionRepository(
regionRepositoryImpl: RegionRepositoryImpl
): RegionRepository


@Binds
@Singleton
fun bindsWalkCourseRepository(
walkCourseRepositoryImpl: WalkCourseRepositoryImpl
): WalkCourseRepository
}
10 changes: 8 additions & 2 deletions app/src/main/java/com/paw/key/data/di/ServiceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

}
Original file line number Diff line number Diff line change
@@ -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<CoordinateDto>,
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
)
}
}

Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -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<WalkCourseResponseDto> {
val jsonString = Json.encodeToString(WalkCourseRequestDto.serializer(), walkCourseRequestDto)
val requestBody = jsonString.toRequestBody("application/json".toMediaType())

return walkCourseService.postWalkCourse(userId, file, requestBody)
}
}
Original file line number Diff line number Diff line change
@@ -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<WalkCourseRegionIdEntity> = runCatching {
walkCourseDataSource.postWalkCourse(userId, image, routeRequestDto)
.data.toEntity()
}
}
Original file line number Diff line number Diff line change
@@ -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<WalkCourseResponseDto>
}
Original file line number Diff line number Diff line change
@@ -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<CoordinateEntity>,
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
)

Original file line number Diff line number Diff line change
@@ -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<WalkCourseRegionIdEntity>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<LatLng> = 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<LatLng> = 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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading