-
Notifications
You must be signed in to change notification settings - Fork 3
[API] 피드 작성 구현 #88
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
[API] 피드 작성 구현 #88
Changes from all commits
267ba77
db81304
32f84f3
e9b34ef
6fdf0ae
4989bb5
60c5a55
395eb3d
3a24db5
4eae562
16247cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.texthip.thip.data.model.feed.request | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class CreateFeedRequest( | ||
| @SerialName("isbn") | ||
| val isbn: String, | ||
| @SerialName("contentBody") | ||
| val contentBody: String, | ||
| @SerialName("isPublic") | ||
| val isPublic: Boolean, | ||
| @SerialName("tagList") | ||
| val tagList: List<String> = emptyList() | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.texthip.thip.data.model.feed.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class CreateFeedResponse( | ||
| @SerialName("feedId") | ||
| val feedId: Int | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.texthip.thip.data.model.feed.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class FeedWriteInfoResponse( | ||
| @SerialName("categoryList") | ||
| val categoryList: List<FeedCategory> | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class FeedCategory( | ||
| @SerialName("category") | ||
| val category: String, | ||
| @SerialName("tagList") | ||
| val tagList: List<String> | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| package com.texthip.thip.data.repository | ||
|
|
||
| import android.content.Context | ||
| import android.net.Uri | ||
| import com.texthip.thip.data.model.base.handleBaseResponse | ||
| import com.texthip.thip.data.model.feed.request.CreateFeedRequest | ||
| import com.texthip.thip.data.model.feed.response.CreateFeedResponse | ||
| import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse | ||
| import com.texthip.thip.data.service.FeedService | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.withContext | ||
| import kotlinx.serialization.json.Json | ||
| import okhttp3.MediaType.Companion.toMediaType | ||
| import okhttp3.MultipartBody | ||
| import okhttp3.RequestBody.Companion.asRequestBody | ||
| import okhttp3.RequestBody.Companion.toRequestBody | ||
| import java.io.File | ||
| import java.io.FileOutputStream | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| class FeedRepository @Inject constructor( | ||
| private val feedService: FeedService, | ||
| @param:ApplicationContext private val context: Context, | ||
| private val json: Json | ||
| ) { | ||
|
|
||
| /** 피드 작성에 필요한 카테고리 및 태그 목록 조회 */ | ||
| suspend fun getFeedWriteInfo(): Result<FeedWriteInfoResponse?> = runCatching { | ||
| val response = feedService.getFeedWriteInfo() | ||
| .handleBaseResponse() | ||
| .getOrThrow() | ||
|
|
||
| // 카테고리 순서 조정 | ||
| val orderedCategories = response?.categoryList?.sortedBy { category -> | ||
| when (category.category) { | ||
| "문학" -> 0 | ||
| "과학·IT" -> 1 | ||
| "사회과학" -> 2 | ||
| "인문학" -> 3 | ||
| "예술" -> 4 | ||
| else -> 999 | ||
| } | ||
| } ?: emptyList() | ||
|
|
||
| response?.copy(categoryList = orderedCategories) | ||
| } | ||
|
|
||
| /** 피드 생성 */ | ||
| suspend fun createFeed( | ||
| isbn: String, | ||
| contentBody: String, | ||
| isPublic: Boolean, | ||
| tagList: List<String>, | ||
| imageUris: List<Uri> | ||
| ): Result<CreateFeedResponse?> = runCatching { | ||
| val request = CreateFeedRequest( | ||
| isbn = isbn, | ||
| contentBody = contentBody, | ||
| isPublic = isPublic, | ||
| tagList = tagList | ||
| ) | ||
|
|
||
| // JSON 요청 부분을 RequestBody로 변환 | ||
| val requestJson = json.encodeToString(CreateFeedRequest.serializer(), request) | ||
| val requestBody = requestJson.toRequestBody("application/json".toMediaType()) | ||
|
Comment on lines
+67
to
+68
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이거 FeedSerivce의 createFeed에서 CreateFeedRequest를 안쓰고 RequestBody를 써서 여기에서 처리한거같은데 그렇게 한 이유가 있을까요? 웬만하면 형식 통일해서 FeedService에서 suspend fun createFeed(
@Part("request") request: CreateFeedRequest, ...이런식으로 쓰는게 좋지 않을까 생각합니당
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이게 제가 @MuitiPart를 사용하고 있어서 원시타입만 허용하기에 이렇게 해야된다고 합니다. RequestBody 같은거만 사용가능하고 커스텀 dto는 사용하면 자동으로 레트로핏이 직렬화가 안된다고 합니다 |
||
|
|
||
| // 임시 파일 목록 추적 | ||
| val tempFiles = mutableListOf<File>() | ||
|
|
||
| try { | ||
| // 이미지 파일들을 MultipartBody.Part로 변환 | ||
| val imageParts = if (imageUris.isNotEmpty()) { | ||
| withContext(Dispatchers.IO) { | ||
| imageUris.mapNotNull { uri -> | ||
| try { | ||
| uriToMultipartBodyPart(uri, "images", tempFiles) | ||
| } catch (e: Exception) { | ||
| null | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| null | ||
| } | ||
|
|
||
| feedService.createFeed(requestBody, imageParts) | ||
| .handleBaseResponse() | ||
| .getOrThrow() | ||
| } finally { | ||
| // 임시 파일들 정리 | ||
| cleanupTempFiles(tempFiles) | ||
| } | ||
| } | ||
|
|
||
| private fun uriToMultipartBodyPart(uri: Uri, paramName: String, tempFiles: MutableList<File>): MultipartBody.Part? { | ||
| return try { | ||
| // MIME 타입 확인 | ||
| val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg" | ||
| val extension = when (mimeType) { | ||
| "image/png" -> "png" | ||
| "image/gif" -> "gif" | ||
| "image/jpeg", "image/jpg" -> "jpg" | ||
| else -> "jpg" // 기본값 | ||
| } | ||
|
|
||
| // 파일명 생성 | ||
| val fileName = "feed_image_${System.currentTimeMillis()}.$extension" | ||
| val tempFile = File(context.cacheDir, fileName) | ||
|
||
|
|
||
| // 임시 파일 목록에 추가 | ||
| tempFiles.add(tempFile) | ||
|
|
||
| // InputStream을 use 블록으로 안전하게 관리 | ||
| context.contentResolver.openInputStream(uri)?.use { inputStream -> | ||
| FileOutputStream(tempFile).use { outputStream -> | ||
| inputStream.copyTo(outputStream) | ||
| } | ||
| } ?: return null | ||
|
|
||
| // MultipartBody.Part 생성 | ||
| val requestFile = tempFile.asRequestBody(mimeType.toMediaType()) | ||
| MultipartBody.Part.createFormData(paramName, fileName, requestFile) | ||
| } catch (e: Exception) { | ||
| e.printStackTrace() | ||
| null | ||
| } | ||
| } | ||
|
|
||
| /** 임시 파일들을 정리하는 함수 */ | ||
| private fun cleanupTempFiles(tempFiles: List<File>) { | ||
| tempFiles.forEach { file -> | ||
| try { | ||
| if (file.exists()) { | ||
| file.delete() | ||
| } | ||
| } catch (e: Exception) { | ||
| e.printStackTrace() | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.texthip.thip.data.service | ||
|
|
||
| import com.texthip.thip.data.model.base.BaseResponse | ||
| import com.texthip.thip.data.model.feed.response.CreateFeedResponse | ||
| import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse | ||
| import okhttp3.MultipartBody | ||
| import okhttp3.RequestBody | ||
| import retrofit2.http.GET | ||
| import retrofit2.http.Multipart | ||
| import retrofit2.http.POST | ||
| import retrofit2.http.Part | ||
|
|
||
| interface FeedService { | ||
|
|
||
| /** 피드 작성에 필요한 카테고리 및 태그 목록 조회 */ | ||
| @GET("feeds/write-info") | ||
| suspend fun getFeedWriteInfo(): BaseResponse<FeedWriteInfoResponse> | ||
|
|
||
| /** 피드 생성 */ | ||
| @Multipart | ||
| @POST("feeds") | ||
| suspend fun createFeed( | ||
| @Part("request") request: RequestBody, | ||
| @Part images: List<MultipartBody.Part>? | ||
| ): BaseResponse<CreateFeedResponse> | ||
|
Comment on lines
+19
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain멀티파트 전송 시 JSON Part/이미지 Part 네이밍과 Content-Type을 확인해주세요
검증용 스크립트를 공유드립니다. 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) JSON RequestBody가 올바른 미디어 타입으로 생성되는지 확인
rg -n -C3 --type=kt -P 'application/json' app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt || true
rg -n -C3 --type=kt -P '(toMediaType|MediaType\.parse|toRequestBody|RequestBody\.create)' app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt || true
# 2) 이미지 Part 네이밍 확인 (images 혹은 images[] 등)
rg -n -C2 --type=kt -P 'MultipartBody\.Part\.createFormData\(' app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt || true
# 3) 빈 이미지 목록 처리 (빈 리스트를 null로 변환하는지) 확인
rg -n -C3 --type=kt -P '(if\s*\(images\.isEmpty\(\)\)|takeIf|takeUnless)' app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt || trueLength of output: 706 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "--- locating FeedRepository.kt ---"
find app -type f -iname 'FeedRepository.kt' -print || true
echo "--- searching repository for likely patterns ---"
rg -n -S -C3 'createFormData\(|MultipartBody\.Part|toRequestBody\(|application/json|toMediaType\(|MediaType\.parse|RequestBody\.create|feeds\b|createFeed\(' || true
echo "--- showing candidate repository files (top 200 lines each) ---"
# show top matches for files that look relevant
rg -n --files-with-matches -S 'createFormData\(|toRequestBody\(|application/json|toMediaType\(|MultipartBody\.Part|RequestBody\.create' || true \
| head -n 20 \
| xargs -r -I{} sh -c 'echo ""; echo "===== {} ====="; sed -n "1,200p" "{}"'Length of output: 21725 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "---- FeedRepository.kt ----"
nl -ba app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt | sed -n '1,240p' || true
echo ""
echo "---- FeedService.kt ----"
nl -ba app/src/main/java/com/texthip/thip/data/service/FeedService.kt | sed -n '1,240p' || true
echo ""
echo "---- FeedWriteViewModel.kt ----"
nl -ba app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt | sed -n '1,420p' || true
echo ""
echo "---- NetworkModule.kt ----"
nl -ba app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt | sed -n '1,200p' || trueLength of output: 1081 멀티파트 전송 점검 결과 — 조치 필요 확인 요약: 레포지토리에서 JSON RequestBody는 "application/json"으로 생성되며, 이미지 파트는 MultipartBody.Part.createFormData(paramName, ...)로 생성되고 빈 이미지 리스트는 null로 처리됩니다. 다음 항목을 확인/수정해주세요.
권장 작업(간단한 코드 예시)
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.mutableStateListOf | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.ui.Alignment | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainCompose 버전/의존성 호환성 확인 권장
다음 스크립트로 Compose 관련 버전/의존성 선언을 확인할 수 있습니다: Also applies to: 23-23 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Compose BOM / 버전 선언 검색 =="
rg -n -C2 -g '!**/build/**' -g '!**/.gradle/**' -P '(compose-bom|androidx\.compose|compose_version|composeOptions|kotlinCompilerExtensionVersion)'
echo
echo "== FlowRow 관련 아티팩트 확인 (foundation-layout vs accompanist-flowlayout) =="
rg -n -C2 -P 'foundation-layout|accompanist-flowlayout' -g '*gradle*' -g '!**/build/**' -g '!**/.gradle/**'Length of output: 264338 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Showing gradle/libs.versions.toml =="
sed -n '1,240p' gradle/libs.versions.toml || true
echo
echo "== Searching for version refs used (composeBom, foundationVersion, composeVersion, kotlinCompilerExtensionVersion) =="
rg -n --hidden -S --no-ignore-vcs -g 'gradle/**' -P '^(composeBom|foundationVersion|compose_version|composeVersion|kotlinCompilerExtensionVersion)\s*=' || true
echo
echo "== Show settings/build files that may apply BOM usage =="
rg -n -C2 -g '!**/build/**' -g '!**/.gradle/**' -P '(platform\(|composeBom|androidx-compose-bom|bom\(platform' || trueLength of output: 5426 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Search for compose BOM usage (platform/enforcedPlatform/libs) in gradle files =="
rg -n --hidden -S -g '!**/build/**' -g '!**/.gradle/**' 'platform\(|enforcedPlatform\(|compose-bom|composeBom|libs\.[A-Za-z0-9._-]*compose' || true
echo
echo "== Show common gradle files if present (first 240 lines) =="
for f in settings.gradle settings.gradle.kts build.gradle build.gradle.kts app/build.gradle app/build.gradle.kts; do
if [ -f "$f" ]; then
echo "---- $f ----"
sed -n '1,240p' "$f"
echo
fi
done
echo "== Search for explicit foundation dependency usages in build files =="
rg -n --hidden -S -g '!**/build/**' -g '!**/.gradle/**' '(androidx\.compose\.foundation|libs\.(foundation|androidx-foundation))' || trueLength of output: 182646 Compose 버전/의존성 정합성 확인 — 하드코딩된 foundation(1.5.0) 제거 권장 간단히: gradle/libs.versions.toml에서 composeBom = "2024.09.00", foundationVersion = "1.9.0-rc01"로 BOM 사용이 확인되었고 app/build.gradle.kts에는 platform(libs.androidx.compose.bom) 적용도 확인되었습니다. 다만 동일 파일에 implementation("androidx.compose.foundation:foundation:1.5.0")가 하드코딩되어 있어 BOM과 버전 혼재(충돌) 가능성이 있습니다. FlowRow / Arrangement.spacedBy(..., Alignment.CenterHorizontally)는 최신 Compose에서 지원되므로 버전 정리가 필요합니다. 조치 권장 위치:
권장 조치:
🤖 Prompt for AI Agents |
||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.unit.dp | ||
|
|
@@ -19,7 +20,7 @@ fun SubGenreChipGrid( | |
| ) { | ||
| FlowRow( | ||
| modifier = Modifier.fillMaxWidth(), | ||
| horizontalArrangement = Arrangement.spacedBy(8.dp), | ||
| horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), | ||
| verticalArrangement = Arrangement.spacedBy(8.dp) | ||
| ) { | ||
| subGenres.forEach { genre -> | ||
|
|
||
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.
전반적으로 runcatching안쓰고 try catch를 썼던데 이유가 있을까용 ? 다른 코드들이랑 통일성있게 runcatching으로 수정하는게 어떨까욧 !?
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.
아 확인했습니다 지피티꺼 수정하는거를 까먹었네요