-
Notifications
You must be signed in to change notification settings - Fork 0
feat: iOS와 로그인 플로우 통일(Guest Mode 구현) #174
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
Changes from 16 commits
0b1c98b
04fea0f
d66bbfb
c927738
4ea2bd0
0d53f60
bd64507
0655dbf
d107b0a
bda573a
e081cd5
6c87639
8416732
38edf2c
74f4d91
e7ece1c
84ef4e5
94317d8
74f49c2
26825c5
ca25219
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,27 @@ | ||||||||||
| package com.ninecraft.booket.core.common.utils | ||||||||||
|
|
||||||||||
| import android.content.Context | ||||||||||
| import androidx.annotation.StringRes | ||||||||||
| import androidx.compose.runtime.Composable | ||||||||||
| import androidx.compose.ui.res.stringResource | ||||||||||
|
|
||||||||||
| // https://www.youtube.com/watch?v=mB1Lej0aDus | ||||||||||
| sealed class UiText { | ||||||||||
| data class DirectString(val value: String) : UiText() | ||||||||||
|
|
||||||||||
| class StringResource( | ||||||||||
| @StringRes val resId: Int, | ||||||||||
| vararg val args: Any, | ||||||||||
| ) : UiText() | ||||||||||
|
Comment on lines
+14
to
+15
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. 🛠️ Refactor suggestion null 포맷 인자 허용을 위해 args 타입을 Any?로 변경하세요. nullable 인자를 넘겨야 하는 리소스 포맷팅(use-case 꽤 있습니다)에서 현재 시그니처(vararg Any)는 null 전달이 불가능합니다. - vararg val args: Any,
+ vararg val args: Any?,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| @Composable | ||||||||||
| fun asString() = when (this) { | ||||||||||
| is DirectString -> value | ||||||||||
| is StringResource -> stringResource(resId, *args) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| fun asString(context: Context) = when (this) { | ||||||||||
| is DirectString -> value | ||||||||||
| is StringResource -> context.getString(resId, *args) | ||||||||||
| } | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package com.ninecraft.booket.core.data.api.repository | ||
|
|
||
| import com.ninecraft.booket.core.model.AutoLoginState | ||
| import com.ninecraft.booket.core.model.UserState | ||
|
Comment on lines
3
to
+4
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. 중복 상태 API 노출(autoLoginState vs userState) — 정리 필요 새 UserState로 전환하면서 기존 autoLoginState가 남아 API가 중복/혼선을 일으킵니다. 제거하거나 @deprecated로 마이그레이션 경로를 명확히 하세요. 권장 수정(완전 제거): -import com.ninecraft.booket.core.model.AutoLoginState
import com.ninecraft.booket.core.model.UserState
@@
- val autoLoginState: Flow<AutoLoginState>
-
val userState: Flow<UserState>
suspend fun getCurrentUserState(): UserState대안: 유지가 필요하다면 @Deprecated("Use userState instead", ReplaceWith("userState"))
val autoLoginState: Flow<AutoLoginState>Also applies to: 14-19 🤖 Prompt for AI Agents |
||
| import kotlinx.coroutines.flow.Flow | ||
|
|
||
| interface AuthRepository { | ||
|
|
@@ -11,4 +12,8 @@ interface AuthRepository { | |
| suspend fun withdraw(): Result<Unit> | ||
|
|
||
| val autoLoginState: Flow<AutoLoginState> | ||
|
|
||
| val userState: Flow<UserState> | ||
|
|
||
| suspend fun getCurrentUserState(): UserState | ||
| } | ||
|
Comment on lines
+16
to
19
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구현/호출부 전반 검증 요청 DefaultAuthRepository 구현과 호출부가 전부 userState/getCurrentUserState로 이행되었는지 확인해 주세요. 🏁 Script executed: #!/bin/bash
# 남은 사용처 확인
rg -nC2 '\bautoLoginState\b'
# 구현 존재 확인
rg -nC3 'class\s+DefaultAuthRepository\b|override\s+val\s+userState|override\s+suspend\s+fun\s+getCurrentUserState'Length of output: 6579 SplashPresenter에서 authRepository.autoLoginState 사용을 모두 제거하고 userState/getCurrentUserState로 교체하세요. 🤖 Prompt for AI Agents
Contributor
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. 자동 로그인 여부와, 로그인 여부는 다름 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. @easyhooon 맞습니다. 자동 로그인 설정과 현재 로그인 상태는 별개의 개념이네요.
✏️ Learnings added
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,8 @@ import com.ninecraft.booket.core.network.response.BookSearchResponse | |||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.network.response.BookSummary | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.network.response.BookUpsertResponse | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.network.response.Category | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.network.response.GuestBookSearchResponse | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.network.response.GuestBookSummary | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.network.response.HomeResponse | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.network.response.LibraryBookSummary | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.network.response.LibraryBooks | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -79,6 +81,35 @@ internal fun BookSummary.toModel(): BookSummaryModel { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal fun GuestBookSearchResponse.toModel(): BookSearchModel { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return BookSearchModel( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| version = version, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| title = title, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pubDate = pubDate, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| totalResults = totalResults, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| startIndex = startIndex, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| itemsPerPage = itemsPerPage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| query = query, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| searchCategoryId = searchCategoryId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| searchCategoryName = searchCategoryName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastPage = lastPage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| books = books.map { it.toModel() }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal fun GuestBookSummary.toModel(): BookSummaryModel { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return BookSummaryModel( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isbn13 = isbn13, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| title = title.decodeHtmlEntities(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| author = author, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| publisher = publisher, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| coverImageUrl = coverImageUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| link = link, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| userBookStatus = "BEFORE_REGISTRATION", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| key = "$title-$isbn13", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+100
to
+111
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. 🛠️ Refactor suggestion 키 계산 시 디코딩 불일치로 동일 도서 중복 키 가능성
internal fun GuestBookSummary.toModel(): BookSummaryModel {
- return BookSummaryModel(
- isbn13 = isbn13,
- title = title.decodeHtmlEntities(),
- author = author,
- publisher = publisher,
- coverImageUrl = coverImageUrl,
- link = link,
- userBookStatus = "BEFORE_REGISTRATION",
- key = "$title-$isbn13",
- )
+ val decodedTitle = title.decodeHtmlEntities()
+ return BookSummaryModel(
+ isbn13 = isbn13,
+ title = decodedTitle,
+ author = author,
+ publisher = publisher,
+ coverImageUrl = coverImageUrl,
+ link = link,
+ userBookStatus = "BEFORE_REGISTRATION",
+ key = "$decodedTitle-$isbn13",
+ )
}또한 상수 문자열 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal fun BookDetailResponse.toModel(): BookDetailModel { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return BookDetailModel( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| version = version, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,19 @@ internal class DefaultBookRepository @Inject constructor( | |
| override val bookRecentSearches = bookRecentSearchDataSource.recentSearches | ||
| override val libraryRecentSearches = libraryRecentSearchDataSource.recentSearches | ||
|
|
||
| override suspend fun searchBookAsGuest( | ||
| query: String, | ||
| start: Int, | ||
| ) = runSuspendCatching { | ||
| val result = service.searchBookAsGuest( | ||
| query = query, | ||
| start = start, | ||
| ).toModel() | ||
|
|
||
| bookRecentSearchDataSource.addRecentSearch(query) | ||
| result | ||
| } | ||
|
Comment on lines
+20
to
+31
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 chainrunSuspendCatching가 suspend 블록을 지원하는지 확인 필요 현재 블록 내부에서 Retrofit suspend API를 호출합니다. runSuspendCatching가 suspend 블록 시그니처(suspend () -> T)를 지원하지 않으면 컴파일/런타임 이슈가 발생합니다. 아래 스크립트로 선언을 확인해 주세요. 필요한 경우 runSuspendCatching를 suspend inline으로 변경해야 합니다. 필요 시 변경 예시(참고: 선언부 파일에서 적용): -@OptIn(ExperimentalContracts::class)
-@Suppress("WRONG_INVOCATION_KIND", "TooGenericExceptionCaught")
-inline fun <T> runSuspendCatching(block: () -> T): Result<T> {
+@OptIn(ExperimentalContracts::class)
+@Suppress("TooGenericExceptionCaught")
+suspend inline fun <T> runSuspendCatching(
+ crossinline block: suspend () -> T
+): Result<T> {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return try {
- Result.success(block())
+ Result.success(block())
} catch (rethrown: CancellationException) {
throw rethrown
} catch (exception: Throwable) {
Result.failure(exception)
}
}🏁 Script executed: #!/bin/bash
# runSuspendCatching 선언부가 suspend 블록을 받는지 확인
rg -nP --glob 'core/common/**' 'runSuspendCatching\s*\(' -C3
# 기대: 선언부에 'suspend' 키워드와 'block: suspend \(\) ->' 형태가 포함되어야 함Length of output: 1058 runSuspendCatching에 suspend 블록 시그니처 지원 추가 필요 - @OptIn(ExperimentalContracts::class)
- @Suppress("WRONG_INVOCATION_KIND", "TooGenericExceptionCaught")
- inline fun <T> runSuspendCatching(block: () -> T): Result<T> {
+ @OptIn(ExperimentalContracts::class)
+ @Suppress("TooGenericExceptionCaught")
+ suspend inline fun <T> runSuspendCatching(
+ crossinline block: suspend () -> T
+ ): Result<T> {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return try {
Result.success(block())🤖 Prompt for AI Agents |
||
|
|
||
| override suspend fun searchBook( | ||
| query: String, | ||
| start: Int, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package com.ninecraft.booket.core.designsystem.component.button | ||
|
|
||
| import androidx.compose.foundation.interaction.MutableInteractionSource | ||
| import androidx.compose.foundation.interaction.collectIsPressedAsState | ||
| import androidx.compose.foundation.layout.Arrangement | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.FlowRow | ||
| import androidx.compose.foundation.layout.IntrinsicSize | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.layout.width | ||
| import androidx.compose.material3.ButtonDefaults | ||
| import androidx.compose.material3.HorizontalDivider | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.material3.TextButton | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.graphics.Color | ||
| import androidx.compose.ui.unit.dp | ||
| import com.ninecraft.booket.core.common.utils.MultipleEventsCutter | ||
| import com.ninecraft.booket.core.common.utils.get | ||
| import com.ninecraft.booket.core.designsystem.ComponentPreview | ||
|
|
||
| @Composable | ||
| fun ReedTextButton( | ||
| onClick: () -> Unit, | ||
| text: String, | ||
| sizeStyle: ButtonSizeStyle, | ||
| colorStyle: ReedButtonColorStyle, | ||
| modifier: Modifier = Modifier, | ||
| enabled: Boolean = true, | ||
| multipleEventsCutterEnabled: Boolean = true, | ||
| ) { | ||
| val multipleEventsCutter = remember { MultipleEventsCutter.get() } | ||
|
|
||
| val interactionSource = remember { MutableInteractionSource() } | ||
| val isPressed by interactionSource.collectIsPressedAsState() | ||
|
|
||
| TextButton( | ||
| onClick = { | ||
| if (multipleEventsCutterEnabled) { | ||
| multipleEventsCutter.processEvent { onClick() } | ||
| } else { | ||
| onClick() | ||
| } | ||
| }, | ||
| modifier = modifier, | ||
| enabled = enabled, | ||
| colors = ButtonDefaults.textButtonColors( | ||
| containerColor = colorStyle.containerColor(isPressed), | ||
| contentColor = colorStyle.contentColor(), | ||
| disabledContentColor = colorStyle.disabledContentColor(), | ||
| disabledContainerColor = colorStyle.disabledContainerColor(), | ||
| ), | ||
|
Comment on lines
+37
to
+55
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. isPressed가 항상 false가 되는 버그 — interactionSource 미전달
아래 수정으로 연결해 주세요: TextButton(
onClick = {
if (multipleEventsCutterEnabled) {
multipleEventsCutter.processEvent { onClick() }
} else {
onClick()
}
},
modifier = modifier,
enabled = enabled,
+ interactionSource = interactionSource,
colors = ButtonDefaults.textButtonColors(
containerColor = colorStyle.containerColor(isPressed),
contentColor = colorStyle.contentColor(),
disabledContentColor = colorStyle.disabledContentColor(),
disabledContainerColor = colorStyle.disabledContainerColor(),
),
contentPadding = sizeStyle.paddingValues,
) {🤖 Prompt for AI Agents |
||
| contentPadding = sizeStyle.paddingValues, | ||
| ) { | ||
| Column( | ||
| modifier = Modifier.width(IntrinsicSize.Max), | ||
| verticalArrangement = Arrangement.spacedBy(1.dp), | ||
| ) { | ||
| Text( | ||
| text = text, | ||
| style = sizeStyle.textStyle.copy( | ||
| color = if (enabled) colorStyle.contentColor() else colorStyle.disabledContentColor(), | ||
| ), | ||
| ) | ||
| HorizontalDivider( | ||
| thickness = 1.dp, | ||
| color = if (enabled) colorStyle.contentColor() else Color.Transparent, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @ComponentPreview | ||
| @Composable | ||
| private fun ReedTextButtonPreview() { | ||
| Column( | ||
| modifier = Modifier.padding(20.dp), | ||
| verticalArrangement = Arrangement.spacedBy(20.dp), | ||
| ) { | ||
| FlowRow( | ||
| horizontalArrangement = Arrangement.spacedBy(20.dp), | ||
| verticalArrangement = Arrangement.spacedBy(20.dp), | ||
| ) { | ||
| ReedTextButton( | ||
| onClick = {}, | ||
| colorStyle = ReedButtonColorStyle.TEXT, | ||
| sizeStyle = largeButtonStyle, | ||
| text = "text button", | ||
| ) | ||
|
|
||
| ReedTextButton( | ||
| onClick = {}, | ||
| enabled = false, | ||
| colorStyle = ReedButtonColorStyle.TEXT, | ||
| sizeStyle = largeButtonStyle, | ||
| text = "text button", | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,4 +27,11 @@ data class BookSummaryModel( | |
| val link: String = "", | ||
| val userBookStatus: String = "", | ||
| val key: String = "", | ||
| ) | ||
| ) { | ||
| val isRegistered: Boolean | ||
| get() = userBookStatus != BEFORE_REGISTRATION | ||
|
Comment on lines
+31
to
+32
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. 기본값으로 인해 isRegistered가 true로 잘못 계산됩니다. 현재 userBookStatus의 기본값이 빈 문자열("")이라 isRegistered가 기본적으로 true가 됩니다. 기본 상태가 미등록임을 의도했다면 userBookStatus의 기본값을 BEFORE_REGISTRATION으로 지정해야 합니다. 다음과 같이 기본값을 조정해 주세요(선택 영역 밖 변경이므로 참고용 스니펫): data class BookSummaryModel(
// ...
- val userBookStatus: String = "",
+ val userBookStatus: String = BEFORE_REGISTRATION,
// ...
)🤖 Prompt for AI Agents |
||
|
|
||
| companion object { | ||
|
Contributor
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. 👍👍 |
||
| const val BEFORE_REGISTRATION = "BEFORE_REGISTRATION" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.ninecraft.booket.core.model | ||
|
|
||
| sealed interface UserState { | ||
| data object Guest : UserState | ||
| data object LoggedIn : UserState | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.ninecraft.booket.core.network.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class GuestBookSearchResponse( | ||
| @SerialName("version") | ||
| val version: String, | ||
| @SerialName("title") | ||
| val title: String, | ||
| @SerialName("pubDate") | ||
| val pubDate: String, | ||
| @SerialName("totalResults") | ||
| val totalResults: Int, | ||
| @SerialName("startIndex") | ||
| val startIndex: Int, | ||
| @SerialName("itemsPerPage") | ||
| val itemsPerPage: Int, | ||
| @SerialName("query") | ||
| val query: String, | ||
| @SerialName("searchCategoryId") | ||
| val searchCategoryId: Int, | ||
| @SerialName("searchCategoryName") | ||
| val searchCategoryName: String, | ||
| @SerialName("lastPage") | ||
| val lastPage: Boolean, | ||
| @SerialName("books") | ||
| val books: List<GuestBookSummary>, | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class GuestBookSummary( | ||
| @SerialName("isbn13") | ||
| val isbn13: String, | ||
| @SerialName("title") | ||
| val title: String, | ||
| @SerialName("author") | ||
| val author: String, | ||
| @SerialName("publisher") | ||
| val publisher: String, | ||
| @SerialName("coverImageUrl") | ||
| val coverImageUrl: String, | ||
| @SerialName("link") | ||
| val link: String, | ||
| ) |
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.
present에 String을 그대로 넣는 부분이 찝찝했었는데 (토끼한테 매번 잡히는 것도 그렇구) 너무 좋네요 👍👍