-
Notifications
You must be signed in to change notification settings - Fork 1
presigned upload API 구현 #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
26e2345
✨ Feat: UploadHttpClientProvider 구현
dogmania d320ef7
✨ Feat: S3 업로드 전용 apiCall 메서드 구현
dogmania c7b5bd6
✨ Feat: S3 업로드 응답 DTO, Domain Model, Mapper 구현
dogmania 13b403b
✨ Feat: PresignedUploader 구현
dogmania a3d1192
✨ Feat: PhotoLogService 구현
dogmania 4c22cbe
✨ Feat: PhotoLogRepository 구현
dogmania e3edd36
✨ Feat: PhotoLogService Koin 컨테이너 등록
dogmania b551d9f
✨ Feat: 이미지 업로드 전용 HttpClient Koin 컨테이너 등록
dogmania 72d5083
✨ Feat: PhotoLogRepository Koin 컨테이너 등록
dogmania e171c90
✨ Feat: upload 메서드 구현
dogmania cf8f632
Merge branch 'develop' of github.com:YAPP-Github/Twix-Android into fe…
dogmania File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
16 changes: 16 additions & 0 deletions
16
core/network/src/main/java/com/twix/network/UploadHttpClientProvider.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.twix.network | ||
|
|
||
| import io.ktor.client.HttpClient | ||
| import io.ktor.client.engine.okhttp.OkHttp | ||
| import io.ktor.client.plugins.HttpTimeout | ||
|
|
||
| object UploadHttpClientProvider { | ||
| fun create(): HttpClient = | ||
| HttpClient(OkHttp) { | ||
| install(HttpTimeout) { | ||
| requestTimeoutMillis = 120_000 | ||
| connectTimeoutMillis = 30_000 | ||
| socketTimeoutMillis = 120_000 | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
core/network/src/main/java/com/twix/network/execute/UploadCall.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package com.twix.network.execute | ||
|
|
||
| import com.twix.result.AppError | ||
| import com.twix.result.AppResult | ||
| import io.ktor.client.plugins.ResponseException | ||
| import io.ktor.client.statement.bodyAsText | ||
| import kotlinx.io.IOException | ||
| import kotlinx.serialization.SerializationException | ||
| import java.net.SocketTimeoutException | ||
| import kotlin.coroutines.cancellation.CancellationException | ||
|
|
||
| suspend inline fun <T> safeUploadCall(crossinline call: suspend () -> T): AppResult<T> = | ||
| try { | ||
| AppResult.Success(call()) | ||
| } catch (e: CancellationException) { | ||
| throw e | ||
| } catch (e: ResponseException) { | ||
| val status = e.response.status.value | ||
| val raw = runCatching { e.response.bodyAsText() }.getOrNull() | ||
|
|
||
| AppResult.Error( | ||
| AppError.Http( | ||
| status = status, | ||
| code = null, | ||
| message = null, | ||
| rawBody = raw, | ||
| ), | ||
| ) | ||
| } catch (e: SocketTimeoutException) { | ||
| AppResult.Error(AppError.Timeout(e)) | ||
| } catch (e: IOException) { | ||
| AppResult.Error(AppError.Network(e)) | ||
| } catch (e: SerializationException) { | ||
| AppResult.Error(AppError.Serialization(e)) | ||
| } catch (e: Throwable) { | ||
| AppResult.Error(AppError.Unknown(e)) | ||
| } |
10 changes: 10 additions & 0 deletions
10
core/network/src/main/java/com/twix/network/model/response/photo/mapper/PhotoMapper.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.twix.network.model.response.photo.mapper | ||
|
|
||
| import com.twix.domain.model.photo.PhotoLogUploadInfo | ||
| import com.twix.network.model.response.photo.model.PhotoLogUploadUrlResponse | ||
|
|
||
| fun PhotoLogUploadUrlResponse.toDomain(): PhotoLogUploadInfo = | ||
| PhotoLogUploadInfo( | ||
| uploadUrl = this.uploadUrl, | ||
| fileName = this.fileName, | ||
| ) |
10 changes: 10 additions & 0 deletions
10
...rk/src/main/java/com/twix/network/model/response/photo/model/PhotoLogUploadUrlResponse.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.twix.network.model.response.photo.model | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class PhotoLogUploadUrlResponse( | ||
| @SerialName("uploadUrl") val uploadUrl: String, | ||
| @SerialName("fileName") val fileName: String, | ||
| ) |
12 changes: 12 additions & 0 deletions
12
core/network/src/main/java/com/twix/network/service/PhotoLogService.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.twix.network.service | ||
|
|
||
| import com.twix.network.model.response.photo.model.PhotoLogUploadUrlResponse | ||
| import de.jensklingenberg.ktorfit.http.GET | ||
| import de.jensklingenberg.ktorfit.http.Query | ||
|
|
||
| interface PhotoLogService { | ||
| @GET("api/v1/photologs/upload-url") | ||
| suspend fun getUploadUrl( | ||
| @Query("goalId") goalId: Long, | ||
| ): PhotoLogUploadUrlResponse | ||
| } |
31 changes: 31 additions & 0 deletions
31
core/network/src/main/java/com/twix/network/upload/PresignedUploader.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package com.twix.network.upload | ||
|
|
||
| import com.twix.network.execute.safeUploadCall | ||
| import com.twix.result.AppResult | ||
| import io.ktor.client.HttpClient | ||
| import io.ktor.client.request.put | ||
| import io.ktor.client.request.setBody | ||
| import io.ktor.http.ContentType | ||
| import io.ktor.http.contentType | ||
| import io.ktor.http.isSuccess | ||
|
|
||
| class PresignedUploader( | ||
| private val client: HttpClient, | ||
| ) { | ||
| suspend fun upload( | ||
| uploadUrl: String, | ||
| bytes: ByteArray, | ||
| contentType: String = "image/jpeg", | ||
| ): AppResult<Unit> = | ||
| safeUploadCall { | ||
| val response = | ||
| client.put(uploadUrl) { | ||
| contentType(ContentType.parse(contentType)) | ||
| setBody(bytes) | ||
| } | ||
|
|
||
| if (!response.status.isSuccess()) { | ||
| throw IllegalStateException("S3 upload failed: ${response.status.value}") | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
data/src/main/java/com/twix/data/repository/DefaultPhotoLogRepository.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.twix.data.repository | ||
|
|
||
| import com.twix.domain.model.photo.PhotoLogUploadInfo | ||
| import com.twix.domain.repository.PhotoLogRepository | ||
| import com.twix.network.execute.safeApiCall | ||
| import com.twix.network.model.response.photo.mapper.toDomain | ||
| import com.twix.network.service.PhotoLogService | ||
| import com.twix.network.upload.PresignedUploader | ||
| import com.twix.result.AppResult | ||
|
|
||
| class DefaultPhotoLogRepository( | ||
| private val service: PhotoLogService, | ||
| private val uploader: PresignedUploader, | ||
| ) : PhotoLogRepository { | ||
| override suspend fun getUploadUrl(goalId: Long): AppResult<PhotoLogUploadInfo> = safeApiCall { service.getUploadUrl(goalId).toDomain() } | ||
|
|
||
| override suspend fun uploadPhotoLogImage( | ||
| goalId: Long, | ||
| bytes: ByteArray, | ||
| contentType: String, | ||
| ): AppResult<String> { | ||
| // 서버에서 presigned url 발급 | ||
| val infoResult = getUploadUrl(goalId) | ||
| val info = | ||
| when (infoResult) { | ||
| is AppResult.Success -> infoResult.data | ||
| is AppResult.Error -> return infoResult | ||
| } | ||
|
|
||
| // S3로 직접 업로드 | ||
| val uploadResult = | ||
| uploader.upload( | ||
| uploadUrl = info.uploadUrl, | ||
| bytes = bytes, | ||
| contentType = contentType, | ||
| ) | ||
| if (uploadResult is AppResult.Error) return uploadResult | ||
|
|
||
| // fileName을 key로 사용함. 인증샷 등록 API에서 사용 | ||
| return AppResult.Success(info.fileName) | ||
| } | ||
| } |
6 changes: 6 additions & 0 deletions
6
domain/src/main/java/com/twix/domain/model/photo/PhotoLogUploadInfo.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.twix.domain.model.photo | ||
|
|
||
| data class PhotoLogUploadInfo( | ||
| val uploadUrl: String = "", | ||
| val fileName: String = "", | ||
| ) |
14 changes: 14 additions & 0 deletions
14
domain/src/main/java/com/twix/domain/repository/PhotoLogRepository.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.twix.domain.repository | ||
|
|
||
| import com.twix.domain.model.photo.PhotoLogUploadInfo | ||
| import com.twix.result.AppResult | ||
|
|
||
| interface PhotoLogRepository { | ||
| suspend fun getUploadUrl(goalId: Long): AppResult<PhotoLogUploadInfo> | ||
|
|
||
| suspend fun uploadPhotoLogImage( | ||
| goalId: Long, | ||
| bytes: ByteArray, | ||
| contentType: String, | ||
| ): AppResult<String> | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Ktor HttpClient expectSuccess default behavior 2.x💡 Result:
In Ktor HttpClient 2.x,
expectSuccessis off by default—meaning Ktor does not automatically treat non-2xx HTTP statuses as failures (it won’t throw just because the status is 3xx/4xx/5xx). You must explicitly enable it. [1]If you set
expectSuccess = true(client-wide or per-request), Ktor enables its default response validation and will throw on non-2xx responses:RedirectResponseException(3xx),ClientRequestException(4xx),ServerResponseException(5xx). [1][2]You can configure it:
HttpClient { expectSuccess = true }[2]HttpRequestBuilder.expectSuccess = ...[3]Sources: [1] [2] [3]
🏁 Script executed:
Repository: YAPP-Github/Twix-Android
Length of output: 50
🏁 Script executed:
Repository: YAPP-Github/Twix-Android
Length of output: 16294
🏁 Script executed:
Repository: YAPP-Github/Twix-Android
Length of output: 618
🏁 Script executed:
Repository: YAPP-Github/Twix-Android
Length of output: 1579
업로드 클라이언트를
expectSuccess = true로 설정하여 에러 처리를 개선하세요.Ktor 2.x는 기본적으로
expectSuccess = false이므로, 현재 코드의isSuccess()검사는 실제로 도달 가능합니다. 하지만IllegalStateException을 throw하면safeUploadCall의Throwable핸들러에 잡혀 HTTP 상태 정보가 손실됩니다.메인 API 클라이언트(
HttpClientProvider)는 이미expectSuccess = true로 설정되어 있습니다. 업로드 클라이언트도 동일하게 설정하면 Ktor가 자동으로ResponseException을 throw하며,safeUploadCall이 이를 잡아AppError.Http로 매핑하므로 HTTP 상태 코드 정보가 보존됩니다.✅ 개선 방안
UploadHttpClientProvider.kt에서:object UploadHttpClientProvider { fun create(): HttpClient = HttpClient(OkHttp) { + expectSuccess = true install(HttpTimeout) { requestTimeoutMillis = 120_000 connectTimeoutMillis = 30_000 socketTimeoutMillis = 120_000 } } }그 후
PresignedUploader.kt에서 수동 검사 제거:safeUploadCall { - val response = - client.put(uploadUrl) { - contentType(ContentType.parse(contentType)) - setBody(bytes) - } - - if (!response.status.isSuccess()) { - throw IllegalStateException("S3 upload failed: ${response.status.value}") - } + client.put(uploadUrl) { + contentType(ContentType.parse(contentType)) + setBody(bytes) + } }🤖 Prompt for AI Agents