Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
aa7347c
[hotfix]: 구글 로그인 sha-1 지문 추가
JJUYAAA Aug 19, 2025
cdeef1a
[hotfix]: 구글 로그인 sha-1 지문 추가
JJUYAAA Aug 19, 2025
bc1dfef
[refactor]: 피드화면 -> 사용자 찾기 화면 이동 (#109)
JJUYAAA Aug 19, 2025
cc9e483
[refactor]: 사용자 찾기 -> 유저 프로필로 화면 이동 (#109)
JJUYAAA Aug 19, 2025
d6496e6
[refactor]: 사용자 찾기 -> 최근 검색어 api 연동 안되어있는 것 추가 (#109)
JJUYAAA Aug 19, 2025
d88b452
[refactor]: 로그아웃 시 직접 토큰 삭제 (#109)
JJUYAAA Aug 19, 2025
ccf33ed
[refactor]: 로그아웃 시 로그인 화면으로 이동하도록 (#109)
JJUYAAA Aug 19, 2025
1150dcf
[refactor]: 프로필 편집 시 에러메시지 파싱되도록 - 화면 재접근 시 오류가 생기는 경우 있음. 다시 수정 필요(#…
JJUYAAA Aug 19, 2025
0d47bfb
[ui]: 튜토리얼 screen 생성 (#109)
JJUYAAA Aug 19, 2025
71f91b9
[refactor]: 뷰모델에 필요없는 함수 제거 (소셜로그인 연동 전 사용한 함수) (#109)
JJUYAAA Aug 19, 2025
073ee4a
[refactor]: SignupDone 수정 - 닉네임,칭호를 뷰모델에서 받아오도록 (#109)
JJUYAAA Aug 19, 2025
c7f5a6d
[refactor]: 스플래시, 로그인, 회원가입, 튜토리얼 navigation 정리 (#109)
JJUYAAA Aug 19, 2025
de36227
[refactor]: 튜토리얼 문구 string 추출 (#109)
JJUYAAA Aug 19, 2025
a43d5e1
[refactor]: 피드 '...더보기' 이미지로 추가(#109)
JJUYAAA Aug 19, 2025
f20f1ef
[refactor]: 이미지 (#109)
JJUYAAA Aug 19, 2025
eb8da7c
[refactor]: 피드 상세보기 <-> 피드스크린 좋아요, 저장 동기화 되지 않는 오류 해결 (#109)
JJUYAAA Aug 19, 2025
1f23e24
[refactor]: 피드 상세보기 <-> 피드스크린 <-> 다른 유저 프로필 화면 좋아요, 저장 동기화 되지 않는 오류 해…
JJUYAAA Aug 19, 2025
7a006de
Merge branch 'develop' into refactor/#109-qa
Nico1eKim Aug 20, 2025
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
8 changes: 8 additions & 0 deletions app/google-services.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
}
},
"oauth_client": [
{
"client_id": "353417813537-gqd3v6turdn9l26rfeorapgj55pe4nd4.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.texthip.thip",
"certificate_hash": "1f2681ba8d7a53b61b3a10214109763221ed6150"
}
},
{
"client_id": "353417813537-lovs0p2tb9kjnjlp493a7098ov0cb2bu.apps.googleusercontent.com",
"client_type": 1,
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/java/com/texthip/thip/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,16 @@ fun RootNavHost() {

// --- 메인 관련 화면들 ---
composable<CommonRoutes.Main> { // MainScreen으로 가는 경로 추가
MainScreen()
MainScreen(
onNavigateToLogin = {
navController.navigate(CommonRoutes.Login) {
// 메인 화면으로 돌아올 수 없도록 모든 화면 기록 삭제
popUpTo(navController.graph.id) {
inclusive = true
}
}
}
)
}
}
}
9 changes: 7 additions & 2 deletions app/src/main/java/com/texthip/thip/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import com.texthip.thip.ui.navigator.MainNavHost
import com.texthip.thip.ui.navigator.extensions.isMainTabRoute

@Composable
fun MainScreen() {
fun MainScreen(
onNavigateToLogin: () -> Unit
) {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Expand All @@ -28,7 +30,10 @@ fun MainScreen() {
containerColor = Color.Transparent
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
MainNavHost(navController)
MainNavHost(
navController = navController,
onNavigateToLogin = onNavigateToLogin
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse
import com.texthip.thip.data.model.feed.response.MyFeedResponse
import com.texthip.thip.data.model.feed.response.RelatedBooksResponse
import com.texthip.thip.data.service.FeedService
import com.texthip.thip.ui.feed.mock.FeedStateUpdateResult
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
Expand All @@ -36,6 +40,8 @@ class FeedRepository @Inject constructor(
@param:ApplicationContext private val context: Context,
private val json: Json
) {
private val _feedStateUpdateResult = MutableSharedFlow<FeedStateUpdateResult>()
val feedStateUpdateResult: Flow<FeedStateUpdateResult> = _feedStateUpdateResult.asSharedFlow()

Comment on lines +43 to 45
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

SharedFlow 설정 기본값으로 인해 emit가 서스펜딩되어 API 호출이 지연/교착될 수 있음

MutableSharedFlow 기본값(replay=0, buffer=0)은 수집자가 없을 때 emit가 suspend됩니다. Repository의 API 호출 흐름이 수집자 존재에 종속되면 위험합니다. 버퍼를 주고 tryEmit을 사용해 비차단으로 바꾸세요.

-private val _feedStateUpdateResult = MutableSharedFlow<FeedStateUpdateResult>()
-val feedStateUpdateResult: Flow<FeedStateUpdateResult> = _feedStateUpdateResult.asSharedFlow()
+private val _feedStateUpdateResult = MutableSharedFlow<FeedStateUpdateResult>(
+    replay = 0,
+    extraBufferCapacity = 64,
+    onBufferOverflow = BufferOverflow.DROP_OLDEST
+)
+val feedStateUpdateResult: Flow<FeedStateUpdateResult> = _feedStateUpdateResult.asSharedFlow()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private val _feedStateUpdateResult = MutableSharedFlow<FeedStateUpdateResult>()
val feedStateUpdateResult: Flow<FeedStateUpdateResult> = _feedStateUpdateResult.asSharedFlow()
private val _feedStateUpdateResult = MutableSharedFlow<FeedStateUpdateResult>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val feedStateUpdateResult: Flow<FeedStateUpdateResult> = _feedStateUpdateResult.asSharedFlow()
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt around
lines 43-45, the MutableSharedFlow is created with default replay=0 and buffer=0
which makes emit suspend when no collectors are present; change the
MutableSharedFlow creation to provide a non-zero buffer (e.g.,
extraBufferCapacity = 1 or more and set onBufferOverflow to DROP_OLDEST or
DROP_LATEST as appropriate) and replace suspend emit calls with
_feedStateUpdateResult.tryEmit(...) so repository API calls are non-blocking;
keep exposing it as a SharedFlow via asSharedFlow() and ensure downstream
callers handle possible dropped events if you choose a drop overflow policy.

/** 피드 작성에 필요한 카테고리 및 태그 목록 조회 */
suspend fun getFeedWriteInfo(): Result<FeedWriteInfoResponse?> = runCatching {
Expand Down Expand Up @@ -198,6 +204,7 @@ class FeedRepository @Inject constructor(
.handleBaseResponse()
.getOrThrow()
}

/** 임시 파일들을 정리하는 함수 */
private fun cleanupTempFiles(tempFiles: List<File>) {
tempFiles.forEach { file ->
Expand Down Expand Up @@ -230,19 +237,60 @@ class FeedRepository @Inject constructor(
.getOrThrow()
}

suspend fun changeFeedLike(feedId: Long, newLikeStatus: Boolean): Result<FeedLikeResponse?> = runCatching {
/*suspend fun changeFeedLike(feedId: Long, newLikeStatus: Boolean): Result<FeedLikeResponse?> = runCatching {
val request = FeedLikeRequest(type = newLikeStatus)
feedService.changeFeedLike(feedId, request)
.handleBaseResponse()
.getOrThrow()
}*/
suspend fun changeFeedLike(
feedId: Long, newLikeStatus: Boolean,
currentLikeCount: Int,
currentIsSaved: Boolean
): Result<FeedLikeResponse?> {
// 👈 3. 기존 로직을 수정하여 성공 시 방송(emit)하도록 변경
return runCatching {
val request = FeedLikeRequest(type = newLikeStatus)
feedService.changeFeedLike(feedId, request)
.handleBaseResponse()
.getOrThrow()
}.onSuccess { response ->
// API 호출 성공 및 응답 데이터가 있을 경우
response?.let {
// 변경된 상태를 객체로 만들어 방송(emit)
val newLikeCount = if (it.isLiked) currentLikeCount + 1 else currentLikeCount - 1
val update = FeedStateUpdateResult(
feedId = feedId,
isLiked = it.isLiked,
likeCount = newLikeCount,
isSaved = currentIsSaved // isSaved 상태는 그대로 유지
)
_feedStateUpdateResult.emit(update)
}
}
}

/** 피드 저장 */
suspend fun changeFeedSave(feedId: Long, newSaveStatus: Boolean): Result<FeedSaveResponse?> = runCatching {
val request = FeedSaveRequest(type = newSaveStatus)
feedService.changeFeedSave(feedId, request)
.handleBaseResponse()
.getOrThrow()
}
suspend fun changeFeedSave(
feedId: Long, newSaveStatus: Boolean, currentIsLiked: Boolean,
currentLikeCount: Int
): Result<FeedSaveResponse?> =
runCatching {
val request = FeedSaveRequest(type = newSaveStatus)
feedService.changeFeedSave(feedId, request)
.handleBaseResponse()
.getOrThrow()
}.onSuccess { response ->
response?.let {
// API 응답(isSaved)과 파라미터로 받은 값들을 조합
val update = FeedStateUpdateResult(
feedId = feedId,
isLiked = currentIsLiked, // isLiked 상태는 그대로 유지
likeCount = currentLikeCount,
isSaved = it.isSaved
)
_feedStateUpdateResult.emit(update)
}
}

Comment on lines +274 to 295
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

저장 상태 이벤트 emit도 비차단으로 변경

SharedFlow 설정과 일관되게 tryEmit 사용을 권장합니다.

-                _feedStateUpdateResult.emit(update)
+                _feedStateUpdateResult.tryEmit(update)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
suspend fun changeFeedSave(
feedId: Long, newSaveStatus: Boolean, currentIsLiked: Boolean,
currentLikeCount: Int
): Result<FeedSaveResponse?> =
runCatching {
val request = FeedSaveRequest(type = newSaveStatus)
feedService.changeFeedSave(feedId, request)
.handleBaseResponse()
.getOrThrow()
}.onSuccess { response ->
response?.let {
// API 응답(isSaved)과 파라미터로 받은 값들을 조합
val update = FeedStateUpdateResult(
feedId = feedId,
isLiked = currentIsLiked, // isLiked 상태는 그대로 유지
likeCount = currentLikeCount,
isSaved = it.isSaved
)
_feedStateUpdateResult.emit(update)
}
}
suspend fun changeFeedSave(
feedId: Long, newSaveStatus: Boolean, currentIsLiked: Boolean,
currentLikeCount: Int
): Result<FeedSaveResponse?> =
runCatching {
val request = FeedSaveRequest(type = newSaveStatus)
feedService.changeFeedSave(feedId, request)
.handleBaseResponse()
.getOrThrow()
}.onSuccess { response ->
response?.let {
// API 응답(isSaved)과 파라미터로 받은 값들을 조합
val update = FeedStateUpdateResult(
feedId = feedId,
isLiked = currentIsLiked, // isLiked 상태는 그대로 유지
likeCount = currentLikeCount,
isSaved = it.isSaved
)
_feedStateUpdateResult.tryEmit(update)
}
}
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt around
lines 274 to 295, the repository currently uses the suspending emit(...) to
publish save-state updates which can block; change this to use the non-blocking
tryEmit(...) consistent with the SharedFlow configuration: replace the call to
_feedStateUpdateResult.emit(update) with _feedStateUpdateResult.tryEmit(update)
so the event is emitted non-blockingly (no additional behavior changes
required).

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.texthip.thip.data.repository

import retrofit2.HttpException
import android.util.Log
import com.texthip.thip.data.manager.TokenManager
import com.texthip.thip.data.model.base.BaseResponse
import com.texthip.thip.data.model.base.ThipApiFailureException
import com.texthip.thip.data.model.base.handleBaseResponse
import com.texthip.thip.data.model.users.request.FollowRequest
import com.texthip.thip.data.model.users.request.NicknameRequest
Expand All @@ -16,13 +19,15 @@ import com.texthip.thip.data.model.users.response.OthersFollowersResponse
import com.texthip.thip.data.model.users.response.SignupResponse
import com.texthip.thip.data.model.users.response.UserSearchResponse
import com.texthip.thip.data.service.UserService
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class UserRepository @Inject constructor(
private val userService: UserService,
private val tokenManager: TokenManager
private val tokenManager: TokenManager,
private val json: Json
) {
//내 팔로잉 목록 조회
suspend fun getMyFollowings(
Expand Down Expand Up @@ -79,13 +84,35 @@ class UserRepository @Inject constructor(
.handleBaseResponse()
.getOrThrow()
}

/*
suspend fun updateProfile(request: ProfileUpdateRequest): Result<Unit?> = runCatching {
userService.updateProfile(request)
.handleBaseResponse()
.getOrThrow()
}*/
suspend fun updateProfile(request: ProfileUpdateRequest): Result<Unit?> {
return try {
val response = userService.updateProfile(request)
response.handleBaseResponse().getOrThrow()
Result.success(Unit)
} catch (e: HttpException) {
val errorBody = e.response()?.errorBody()?.string()
if (errorBody != null) {
try {
val baseResponse = json.decodeFromString<BaseResponse<Unit>>(errorBody)
Result.failure(ThipApiFailureException(baseResponse.code, baseResponse.message))
} catch (jsonException: Exception) {
Result.failure(e) // JSON 파싱 실패 시 원래 HttpException 반환
}
} else {
Result.failure(e) // 에러 본문이 없으면 원래 HttpException 반환
}
} catch (e: Exception) {
Result.failure(e)
}
}


suspend fun signup(request: SignupRequest): Result<SignupResponse?> {
Log.d("SignupDebug", "UserRepository.signup() 호출됨. 요청 닉네임: ${request.nickname}")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ fun ImageViewerModal(
.fillMaxSize()
.clickable { onDismiss() }
) {
// 닫기 버튼
// 이전 버튼
Icon(
painter = painterResource(R.drawable.ic_x),
painter = painterResource(R.drawable.ic_arrow_back),
contentDescription = "닫기",
tint = colors.White,
modifier = Modifier
.align(Alignment.TopEnd)
.align(Alignment.TopStart)
.padding(20.dp)
.size(24.dp)
.clickable { onDismiss() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,17 @@ fun SearchPeopleResultPreview() {
SearchPeopleResult(
peopleList = listOf(
MySubscriptionData(
userId = 1L,
profileImageUrl = null,
nickname = "Thiper_Official",
role = "공식 인플루언서",
roleColor = colors.NeonGreen,
subscriberCount = 50,
isSubscribed = false
isSubscribed = false,

),
MySubscriptionData(
userId = 1L,
profileImageUrl = null,
nickname = "Thiper_Writer",
role = "작가",
Expand All @@ -76,6 +79,7 @@ fun SearchPeopleResultPreview() {
isSubscribed = true
),
MySubscriptionData(
userId = 1L,
profileImageUrl = null,
nickname = "Thiper_Newbie",
role = "칭호칭호",
Expand Down
Loading