Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.texthip.thip.data.di
import com.texthip.thip.data.service.BookService
import com.texthip.thip.data.service.RecentSearchService
import com.texthip.thip.data.service.CommentsService
import com.texthip.thip.data.service.FeedService
import com.texthip.thip.data.service.RoomsService
import com.texthip.thip.data.service.UserService
import dagger.Module
Expand Down Expand Up @@ -42,4 +43,9 @@ object ServiceModule {
@Singleton
fun providesCommentsService(retrofit: Retrofit): CommentsService =
retrofit.create(CommentsService::class.java)

@Provides
@Singleton
fun provideFeedService(retrofit: Retrofit): FeedService =
retrofit.create(FeedService::class.java)
}
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>
)
144 changes: 144 additions & 0 deletions app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전반적으로 runcatching안쓰고 try catch를 썼던데 이유가 있을까용 ? 다른 코드들이랑 통일성있게 runcatching으로 수정하는게 어떨까욧 !?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 확인했습니다 지피티꺼 수정하는거를 까먹었네요

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 FeedSerivce의 createFeed에서 CreateFeedRequest를 안쓰고 RequestBody를 써서 여기에서 처리한거같은데 그렇게 한 이유가 있을까요? 웬만하면 형식 통일해서 FeedService에서

suspend fun createFeed(
        @Part("request") request: CreateFeedRequest, ...

이런식으로 쓰는게 좋지 않을까 생각합니당

Copy link
Copy Markdown
Collaborator Author

@rbqks529 rbqks529 Aug 17, 2025

Choose a reason for hiding this comment

The 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)
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temporary files created in cache directory are not being cleaned up. Consider implementing cleanup logic or using try-with-resources pattern to ensure proper resource management.

Copilot uses AI. Check for mistakes.

// 임시 파일 목록에 추가
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()
}
}
}
}
26 changes: 26 additions & 0 deletions app/src/main/java/com/texthip/thip/data/service/FeedService.kt
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

멀티파트 전송 시 JSON Part/이미지 Part 네이밍과 Content-Type을 확인해주세요

  • @part("request") RequestBody: Repository에서 반드시 "application/json; charset=UTF-8"로 생성되었는지 확인 필요합니다. 잘못된 미디어 타입이면 서버가 파싱하지 못할 수 있어요.
  • @part images: List<MultipartBody.Part>?: MultipartBody.Part 사용은 올바르며, 각 Part 생성 시 createFormData("images", filename, body)처럼 서버 스펙과 동일한 키를 써야 합니다. 서버가 images[] 형태를 요구하면 그에 맞춰 name을 설정해야 합니다.
  • 빈 이미지일 때: null을 넘겨 Part를 생략하는 현재 설계는 흔히 쓰이는 방식입니다. Repository에서 빈 리스트를 null로 변환해 주는지 확인해 주세요.

검증용 스크립트를 공유드립니다.


🏁 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 || true

Length 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' || true

Length of output: 1081


멀티파트 전송 점검 결과 — 조치 필요

확인 요약: 레포지토리에서 JSON RequestBody는 "application/json"으로 생성되며, 이미지 파트는 MultipartBody.Part.createFormData(paramName, ...)로 생성되고 빈 이미지 리스트는 null로 처리됩니다. 다음 항목을 확인/수정해주세요.

  • app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt

    • 63–66: requestJson.toRequestBody("application/json".toMediaType()) — 현재 Content-Type이 "application/json"입니다. 서버가 charset을 요구하면 "application/json; charset=UTF-8"로 변경하세요.
    • 68–80: imageUris -> imageParts 매핑; imageUris가 비어있으면 imageParts = null로 feedService.createFeed(requestBody, imageParts) 호출(빈 이미지 시 Part 생략) — 의도된 동작으로 보입니다.
    • 85–112: private fun uriToMultipartBodyPart(uri: Uri, paramName: String) { … MultipartBody.Part.createFormData(paramName, fileName, requestFile) } — createFormData에 전달되는 name은 paramName 변수입니다. 호출부에서 어떤 문자열(예: "images" 또는 "images[]")을 넘기는지 확인하세요.
  • app/src/main/java/com/texthip/thip/data/service/FeedService.kt

    • 19–25: API 시그니처는 @part("request") request: RequestBody, @part images: List<MultipartBody.Part>? — RequestBody 파트명이 "request"로 전송되는 점과 images 파트가 nullable인 점을 백엔드 스펙과 대조해 주세요.
  • app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt

    • 66: json.asConverterFactory(requireNotNull("application/json".toMediaType())) — ConverterFactory의 미디어타입도 "application/json"입니다. RequestBody와 일관되게 필요 시 charset 추가 고려.

권장 작업(간단한 코드 예시)

  • RequestBody에 charset 추가(필요한 경우):
    requestJson.toRequestBody("application/json; charset=UTF-8".toMediaType())
  • 이미지 파트명이 서버 스펙과 일치하는지 확인(예: uriToMultipartBodyPart(uri, "images") 또는 "images[]")
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/service/FeedService.kt around lines
19 to 25, the multipart API signature currently uses @Part("request") request:
RequestBody and @Part images: List<MultipartBody.Part>?; verify and align these
names and nullability with the backend contract: if the backend expects the JSON
part under a different part name or requires charset, create the RequestBody
with "application/json; charset=UTF-8" and use that part name here; ensure the
image part name passed when creating MultipartBody.Part (e.g., "images" vs
"images[]") matches this @Part name, and keep the images parameter
nullable/omitted when no files are present so the feedService.createFeed call
omits empty parts; also ensure NetworkModule converter media type uses the same
media type (add charset if required) for consistency.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Compose 버전/의존성 호환성 확인 권장

Arrangement.spacedBy(…, Alignment.CenterHorizontally) 시그니처와 foundation.layout.FlowRow 사용은 특정 Compose 버전 이상에서만 지원됩니다. 프로젝트의 Compose BOM/버전을 한 번 점검해 주세요.

다음 스크립트로 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' || true

Length 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))' || true

Length 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에서 지원되므로 버전 정리가 필요합니다.

조치 권장 위치:

  • gradle/libs.versions.toml
    • composeBom = "2024.09.00"
    • foundationVersion = "1.9.0-rc01"
  • app/build.gradle.kts
    • implementation(platform(libs.androidx.compose.bom)) — BOM 적용 확인
    • implementation("androidx.compose.foundation:foundation:1.5.0") — 제거 또는 정리 필요
  • UI 사용 파일 (예): app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt (FlowRow/Arrangement 사용)

권장 조치:

  • app/build.gradle.kts에서 explicit한 implementation("androidx.compose.foundation:foundation:1.5.0") 제거하고 BOM(libs.foundation / libs.androidx.foundation)으로 버전 통일 또는 명시적 버전들을 BOM과 맞추기.
  • 필요시 Compose 컴파일러(kotlinCompilerExtensionVersion)와 Kotlin 플러그인 버전도 BOM과 호환되는지 함께 확인하세요.
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt
around line 9 the project is referencing Compose foundation APIs but the build
uses a hardcoded implementation("androidx.compose.foundation:foundation:1.5.0")
which conflicts with the BOM (composeBom = "2024.09.00" and foundationVersion =
"1.9.0-rc01") — remove the explicit foundation dependency from
app/build.gradle.kts and rely on the BOM (use
implementation(platform(libs.androidx.compose.bom)) and libs.foundation or
libs.androidx.foundation), or align the explicit version to the BOM; also verify
and, if necessary, update kotlinCompilerExtensionVersion and Kotlin plugin
versions to be compatible with the BOM so FlowRow/Arrangement APIs resolve
against the BOM-provided Compose versions.

import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
Expand All @@ -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 ->
Expand Down
Loading