Skip to content

[feat] #94 이용약관 API 연동#96

Merged
Ojongseok merged 21 commits into
developfrom
feat/#94-terms-api
Feb 11, 2026
Merged

[feat] #94 이용약관 API 연동#96
Ojongseok merged 21 commits into
developfrom
feat/#94-terms-api

Conversation

@Ojongseok
Copy link
Copy Markdown
Member

@Ojongseok Ojongseok commented Feb 10, 2026

🔗 관련 이슈

📙 작업 설명

  • 이용약관 조회 및 동의 API 연동
    • GET /api/terms: 이용약관 목록 조회
    • POST /api/terms/agreements: 이용약관 동의
  • 로그인 화면 / 이용약관 화면에 대해 뷰모델 분리 (LoginViewModel / TermViewModel)
  • 로그인 시점 변경: 카카오 로그인 직후 서비스 가입 후 약관 동의 여부에 따라 화면 분기
    • agreeTerms == true: 메인 화면으로 이동
    • agreeTerms == false: 약관 화면으로 이동
  • 온보딩 디자인 리소스 SVG → PNG 교체
  • UserInfo 모델에 agreeTerms 필드 추가

💬 추가 설명 or 리뷰 포인트 (선택)

  • 이용약관 조회/동의와 관련해 기존에 앱에서 하드코딩된 부분이 API를 통해 진행되도록 변경되었습니다. UI는 이전과 동일합니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 약관 동의 화면 추가: 이용약관 목록 확인 및 선택·동의 가능.
    • 약관 동의 API 연동: 원격에서 약관을 불러오고 선택한 약관을 제출.
  • 개선사항

    • 로그인/가입 흐름 개선: 가입 후 필수 약관 동의 여부를 확인해 적절히 화면 전환.
    • 사용자 정보 확장: 필수 약관 동의 상태가 사용자 정보에 반영됨.
    • 온보딩 이미지 리소스 갱신: 온보딩 아이콘이 이미지 자원으로 교체됨.

@Ojongseok Ojongseok requested a review from ikseong00 February 10, 2026 09:45
@Ojongseok Ojongseok self-assigned this Feb 10, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 10, 2026

Walkthrough

이용약관 조회/동의 서버 연동과 관련된 도메인 모델, 원격 API, 저장소 구현, UI(약관 화면) 및 내비게이션/리소스 변경을 추가합니다.

Changes

Cohort / File(s) Summary
데이터 API 인터페이스
core/data-api/src/main/java/.../TermRepository.kt
새 public 인터페이스 TermRepository 추가: suspend fun getTerms(): Result<List<Term>>, suspend fun agreeTerms(termIds: List<Long>): Result<Unit>
원격 API & 네트워크 모델
core/data/src/main/java/.../TermService.kt, core/data/src/main/java/.../TermAgreementsRequest.kt, core/data/src/main/java/.../response/TermResponse.kt
TermService 추가( GET /api/terms, POST /api/terms/agreements ); 요청/응답 DTO(TermAgreementsRequest, TermsResponse/TermResponse)와 매핑 함수 추가
네트워크 구성
core/data/src/main/java/.../NetworkModule.kt
인증 없이 전송할 URL 목록에 "/api/terms" 항목 추가
저장소 구현 & DI
core/data/src/main/java/.../impl/TermRepositoryImpl.kt, core/data/src/main/java/.../repository/di/RepositoryModule.kt
TermRepositoryImpl 구현 추가( TermService 사용, runSuspendCatching로 결과 래핑) 및 DI 바인딩 추가
도메인 모델 변경
core/model/src/main/java/.../Term.kt, core/model/src/main/java/.../UserInfo.kt
불변 Term 데이터클래스 추가; UserInfoisRequiredTermsAgreed(기본 false) 필드 및 @Immutable 적용
인증 흐름(로그인) 변경
feature/auth/impl/.../LoginContract.kt, .../LoginScreen.kt, .../LoginViewModel.kt
로그인 계약에서 약관 관련 상태/인텐트 제거; signUp 흐름으로 토큰 저장 후 사용자 약관 동의 상태 확인 로직 추가; navigateToMain 파라미터 추가
이용약관 화면 및 로직(신규)
feature/auth/impl/.../term/TermContract.kt, .../TermScreen.kt, .../TermViewModel.kt
TermState/TermIntent/TermSideEffect 추가, TermViewModel MVI 구현(약관 로드, 개별/전체 토글, 동의 제출 및 네비게이션)
약관 UI 컴포넌트 변경
feature/auth/impl/.../term/component/AgreementSection.kt, .../TermContent.kt
기존 TermAgreement 열거형 사용에서 Term 모델 사용으로 시그니처·콜백·타입 변경
열거형 제거
feature/auth/impl/.../term/model/TermAgreement.kt
TermAgreement enum 및 관련 companion 삭제
내비게이션 설정 변경
feature/auth/impl/.../navigation/AuthEntryProvider.kt
LoginRoutenavigateToMain 추가, HiltSharedViewModelStoreNavEntryDecorator 제거(엔트리 단순화)
온보딩 리소스 및 참조 변경
core/designsystem/src/main/res/drawable/icon_onboarding_*.xml, feature/auth/impl/.../onboarding/*
온보딩 아이콘 3개 삭제(icon_onboarding_*), 코드에서는 image_onboarding_*로 리소스 참조 전환

Sequence Diagram

sequenceDiagram
    participant User
    participant TermScreen
    participant TermViewModel
    participant TermRepository
    participant TermService
    participant Server

    User->>TermScreen: 화면 진입 (EnterTermScreen)
    TermScreen->>TermViewModel: dispatch(EnterTermScreen)
    TermViewModel->>TermRepository: getTerms()
    TermRepository->>TermService: getTerms()
    TermService->>Server: GET /api/terms
    Server-->>TermService: TermsResponse
    TermService-->>TermRepository: BasicResponse<TermsResponse>
    TermRepository-->>TermViewModel: Result<List<Term>>
    TermViewModel->>TermScreen: 상태 업데이트(terms)

    User->>TermScreen: 약관 체크/전체동의
    TermScreen->>TermViewModel: dispatch(ClickAgreeTerm / ClickAgreeAll)
    TermViewModel->>TermScreen: 상태 업데이트(isChecked)

    User->>TermScreen: ClickNext
    TermScreen->>TermViewModel: dispatch(ClickNext)
    TermViewModel->>TermRepository: agreeTerms(termIds)
    TermRepository->>TermService: agreeTerms(request)
    TermService->>Server: POST /api/terms/agreements
    Server-->>TermService: BasicNullableResponse<Unit>
    TermService-->>TermRepository: Result<Unit>
    TermRepository-->>TermViewModel: success
    TermViewModel->>TermScreen: NavigateToMain
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 서버에 묻고, 토끼가 답하네
약관을 모아 하나로 깡충깡충
체크하면 전송, 동의하면 출발
메인으로 향하는 당근 길! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 '[feat] #94 이용약관 API 연동'으로 주요 변경사항인 이용약관 API 연동을 명확하게 나타냅니다.
Linked Issues check ✅ Passed 이용약관 조회 API, 동의 API, 사용자 조회 API 연동 요구사항이 모두 구현되었습니다. TermRepository 인터페이스 및 구현체, TermService, 응답 모델, UserInfo 업데이트 등이 포함됩니다.
Out of Scope Changes check ✅ Passed 온보딩 리소스 파일 교체(icon → image)와 LoginViewModel/TermViewModel 분리는 이용약관 API 연동과 관련된 필수 변경사항입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#94-terms-api

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In
`@core/data/src/main/java/com/neki/android/core/data/repository/impl/TermRepositoryImpl.kt`:
- Around line 17-24: The agreeTerms implementation returns the raw
BasicNullableResponse<Unit> instead of extracting its .data; inside agreeTerms
(which builds TermAgreementsRequest and calls termService.agreeTerms) unwrap the
service response and return its .data (or map to Unit) so the runSuspendCatching
block yields Result<Unit>, mirroring how FolderRepositoryImpl.createFolder and
PhotoRepositoryImpl.registerPhoto extract .data from their service responses.

In
`@feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/term/component/TermContent.kt`:
- Around line 83-89: Add an error field to TermState (e.g., error: String? or
Throwable?), update TermViewModel.fetchTerms() to set that error on API failure
(instead of only logging) and clear it on retry/success, and make TermScreen
observe TermState.error to show an error UI or a Toast (similar to agreeTerms()
behavior) while preserving LoadingDialog; locate and modify TermState,
TermViewModel.fetchTerms(), and TermScreen (and the AgreementSection list in
TermContent.kt) so the UI renders a user-facing error message when fetchTerms
fails.

In
`@feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/term/TermContract.kt`:
- Around line 11-12: The computed property isAllRequiredChecked currently
returns true when there are no required terms because terms.filter {
it.isRequired }.all { it.isChecked } is vacuously true; update the property in
TermContract (isAllRequiredChecked) to first ensure there is at least one
required term (e.g., terms.any { it.isRequired }) and only then return whether
all required terms are checked (terms.filter { it.isRequired }.all {
it.isChecked }) so that if no required terms exist the property returns false
and the "Next" button won’t be enabled.

In
`@feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/term/TermScreen.kt`:
- Around line 40-43: TermSideEffect.NavigateUrl handling currently calls
Intent(Intent.ACTION_VIEW, Uri.parse(sideEffect.url)) and
context.startActivity(intent) without guarding against invalid URIs or missing
handlers; wrap the start flow in a try-catch that catches
ActivityNotFoundException (and optionally
IllegalArgumentException/URISyntaxException) around context.startActivity,
validate or safely parse the URL before creating the Intent, and provide a
fallback (e.g., show a toast/error via viewModel/event or log the error) so
TermSideEffect.NavigateUrl does not crash the app when Uri.parse(sideEffect.url)
or startActivity fails.

In
`@feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/term/TermViewModel.kt`:
- Around line 68-80: fetchTerms currently logs failures but provides no user
feedback; update the TermViewModel.fetchTerms signature to accept a
postSideEffect: (TermSideEffect) -> Unit (or the existing side-effect poster
used elsewhere) and call postSideEffect(...) inside the .onFailure block (after
reduce { copy(isLoading = false) }) to emit the same toast/error side effect
used by agreeTerms; then update all places that call fetchTerms (notably the
onIntent handler) to pass the postSideEffect parameter through so failures show
a toast to the user.
🧹 Nitpick comments (3)
feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/login/LoginViewModel.kt (2)

40-59: 카카오 로그인 중복 호출 방지가 필요합니다.

signUpviewModelScope.launch로 비동기 실행되므로, 사용자가 버튼을 빠르게 연타하면 isLoading 상태 반영 전에 코루틴이 중복 실행될 수 있습니다. state.isLoadingtrue이면 early return하는 가드를 추가하는 것을 권장합니다.

♻️ 수정 제안
     private fun signUp(
         kakaoIdToken: String,
         reduce: (LoginState.() -> LoginState) -> Unit,
         postSideEffect: (LoginSideEffect) -> Unit,
-    ) = viewModelScope.launch {
+    ) {
+        if (store.state.isLoading) return
+        viewModelScope.launch {
         reduce { copy(isLoading = true) }
         authRepository.loginWithKakao(kakaoIdToken)
             .onSuccess { authResult ->
                 tokenRepository.saveTokens(
                     accessToken = authResult.accessToken,
                     refreshToken = authResult.refreshToken,
                 )
                 checkTermAgreementState(postSideEffect)
             }
             .onFailure { exception ->
                 Timber.e(exception)
                 postSideEffect(LoginSideEffect.ShowToastMessage("가입에 실패했습니다. 다시 시도해주세요."))
             }
         reduce { copy(isLoading = false) }
+        }
     }

61-76: getUserInfo 실패 시 약관 화면으로 이동하는 폴백 동작을 확인해 주세요.

getUserInfo() 실패 시 무조건 NavigateToTerm으로 이동하는데, 네트워크 오류나 토큰 만료 등 다양한 원인이 있을 수 있습니다. 이 경우 약관 화면에서도 API 호출이 실패할 가능성이 높으므로, 사용자에게 에러 토스트를 함께 표시하거나 재시도 가능한 경로를 제공하는 것이 좋을 수 있습니다.

core/model/src/main/java/com/neki/android/core/model/Term.kt (1)

1-12: isChecked는 UI 상태이므로 도메인 모델에 포함하는 것이 적절한지 검토 필요.

isChecked는 사용자의 체크박스 선택 여부를 나타내는 프레젠테이션 레이어 상태입니다. 도메인 모델(Term)에 포함하면 서버 응답 매핑과 UI 상태가 혼재됩니다. 현재 규모에서는 실용적인 접근이지만, 향후 ViewModel 내부의 별도 UI 상태(Map<Long, Boolean> 등)로 분리하면 관심사 분리가 명확해집니다.

Copy link
Copy Markdown
Contributor

@ikseong00 ikseong00 left a comment

Choose a reason for hiding this comment

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

기존 코드를 리팩토링하는 형태라 크게 리뷰할 사항은 없어보이네요, 좋습니다!

다만, when 절에서, -> 이후에 한 줄만 오는 경우에 대해서 2가지 케이스가 있는데,
이거는 하나로 맞추는 방향으로 가면 좋을 것 같아요!
개인적으로는 라인 수는 적게 하는 편을 선호했는데, 괄호가 있으면 가독성이 조금 더 좋아지고, 추후 Intent에 의한 작업이 추가되었을 때(ex) reduce), 수정 사항이 한 줄만 있을 것 같다는 생각도 드네요

TermIntent.EnterTermScreen -> fetchTerms(reduce)

TermIntent.ClickBack -> {
    postSideEffect(TermSideEffect.NavigateBack)
}

Comment thread core/model/src/main/java/com/neki/android/core/model/UserInfo.kt Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/term/TermViewModel.kt`:
- Around line 21-26: fetchTerms() currently only logs errors with
Timber.e(error) and provides no user feedback; update the error branch in
fetchTerms() to emit a UI side-effect similar to agreeTerms() by dispatching
TermSideEffect.ShowToastMessage(...) (or an appropriate error message) via the
store or side-effect emitter used in this ViewModel so the user is informed when
fetching terms fails; locate the fetchTerms() method in TermViewModel and add
the side-effect emission where Timber.e(error) is called.
🧹 Nitpick comments (2)
feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/login/LoginViewModel.kt (1)

61-76: getUserInfo() 실패 시 약관 화면으로 이동하는 폴백 전략이 합리적입니다.

사용자 정보 조회 실패 시 약관 화면으로 안내하는 것은 안전한 기본 동작입니다. 다만, 이미 약관에 동의한 사용자가 네트워크 오류 등으로 인해 불필요하게 약관 화면을 다시 보게 될 수 있습니다. 필요 시 재시도 로직 추가를 고려해 보세요.

feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/term/TermViewModel.kt (1)

37-43: "약관 전체 동의"가 필수 약관만 토글합니다.

ClickAgreeAll 처리 시 isRequired인 약관만 토글하고 선택 약관은 변경하지 않습니다. UI에서 "약관 전체 동의"라고 표시되므로 사용자 기대와 실제 동작이 다를 수 있습니다. 의도된 동작이라면 주석을 추가하거나, 전체 약관을 토글하도록 수정을 고려해 주세요.

전체 약관 토글 시 제안
             TermIntent.ClickAgreeAll -> {
-                val shouldCheckAll = !state.isAllRequiredTermChecked
+                val allChecked = state.terms.all { it.isChecked }
                 val updatedTerms = state.terms.map { term ->
-                    if (term.isRequired) term.copy(isChecked = shouldCheckAll) else term
+                    term.copy(isChecked = !allChecked)
                 }.toImmutableList()
                 reduce { copy(terms = updatedTerms) }
             }

Comment on lines +21 to +26
val store: MviIntentStore<TermState, TermIntent, TermSideEffect> =
mviIntentStore(
initialState = TermState(),
onIntent = ::onIntent,
initialFetchData = { store.onIntent(TermIntent.EnterTermScreen) },
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.kt" | xargs grep -l "MviIntentStoreImpl" | head -5

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 145


🏁 Script executed:

rg -n "class MviIntentStoreImpl" --type=kt -A 30

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 105


🏁 Script executed:

rg -n "initialFetchData" --type=kt -B 2 -A 5

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 105


🏁 Script executed:

cat ./core/ui/src/main/java/com/neki/android/core/ui/MviIntentStore.kt

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 2468


🏁 Script executed:

rg -n "initialFetchData" -A 10 --glob="*.kt"

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 16794


🏁 Script executed:

cat feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/term/TermViewModel.kt

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 3710


fetchTerms() 에러 처리에서 사용자 피드백 부재

현재 fetchTerms() 메서드는 실패 시 Timber.e(error) 만 호출하고 있습니다. 반면 agreeTerms() 메서드는 실패 시 TermSideEffect.ShowToastMessage() 로 사용자에게 피드백을 제공합니다. 오류 발생 시 사용자에게 상태를 알릴 수 있도록 약관 조회 실패에 대한 피드백을 추가해주세요.

🤖 Prompt for AI Agents
In
`@feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/term/TermViewModel.kt`
around lines 21 - 26, fetchTerms() currently only logs errors with
Timber.e(error) and provides no user feedback; update the error branch in
fetchTerms() to emit a UI side-effect similar to agreeTerms() by dispatching
TermSideEffect.ShowToastMessage(...) (or an appropriate error message) via the
store or side-effect emitter used in this ViewModel so the user is informed when
fetching terms fails; locate the fetchTerms() method in TermViewModel and add
the side-effect emission where Timber.e(error) is called.

Copy link
Copy Markdown
Member Author

@Ojongseok Ojongseok left a comment

Choose a reason for hiding this comment

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

사실 저는 -> 이후 한줄로 작성이 가능한 경우에도 {} 괄호를 여는 것이 습관이 되었는데 익성님 코드를 보면서 괄호를 열지 않는 것이 라인이 많이 줄어드는 것 같아 한줄로 작성하려 신경을 쓰고 있었는데 해당 부분에서는 놓쳤군요..

다만, 말씀하신 것 처럼 괄호를 여는 것이 가독성은 확실히 좋은 것 같고, 이후 코드가 수정되었을 경우에 수정사항이 줄어든다는 장점이 있는 것 같습니다. 그치만 현재 저희 lint 룰에서는 {...} 중괄호 이후 다음 블럭 이전에 개행이 한줄 더 필요합니다

...
is TermIntent.ClickAgreeTerm -> {
    reduce { copy(terms = updatedTerms) }
}

is TermIntent.ClickTermNavigateUrl -> {
    postSideEffect(TermSideEffect.NavigateUrl(intent.term.url))
}

이런 식으로 말이죠. 코드 라인이 배로 길어지게 될 것 같아요 ㅎㅎ...
한줄로 커버가 가능한 경우에는 대부분 postSideEffect() or reduce {}가 대부분이라 한줄로 적는다고 해서 가독성이 크게 떨어지지는 않는 것 같습니다-! 한줄로 작성하는 것으로 통일하는거 어떠실까요?

통일한다면 Intent, Effect when절 모두에 적용하면 될 것 같습니다!

Comment thread core/model/src/main/java/com/neki/android/core/model/UserInfo.kt Outdated
@ikseong00
Copy link
Copy Markdown
Contributor

ikseong00 commented Feb 11, 2026

이런 식으로 말이죠. 코드 라인이 배로 길어지게 될 것 같아요 ㅎㅎ...
한줄로 커버가 가능한 경우에는 대부분 postSideEffect() or reduce {}가 대부분이라 한줄로 적는다고 해서 가독성이 크게 떨어지지는 않는 것 같습니다-! 한줄로 작성하는 것으로 통일하는거 어떠실까요?
통일한다면 Intent, Effect when절 모두에 적용하면 될 것 같습니다!

좋습니다!!

이 부분도 이슈업하고 클로드 룰에 추가하면 어떨까요?

@Ojongseok
Copy link
Copy Markdown
Member Author

@ikseong00 #103 , #104 해당 PR에서 협의한 내용 이슈로 등록해 두었고, 해당 PR은 병합하도록 하겠습니다.

@Ojongseok Ojongseok merged commit 6b1bfd5 into develop Feb 11, 2026
6 checks passed
@Ojongseok Ojongseok deleted the feat/#94-terms-api branch February 11, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 이용약관 서버 API 연결

2 participants