Skip to content

Feature/110 #130

Open
KateteDeveloper wants to merge 35 commits into
developfrom
feature/110
Open

Feature/110 #130
KateteDeveloper wants to merge 35 commits into
developfrom
feature/110

Conversation

@KateteDeveloper
Copy link
Copy Markdown
Collaborator

@KateteDeveloper KateteDeveloper commented May 17, 2026

📝 설명

기존 코드는 각 RepositoryImpl이 authPreference를 직접 들고 다니며 토큰을 관리하는 구조였습니다.
모든 impl에 토큰 로직이 중복되어 있었고, withAuth / withErrorHandling / withTokenRefresh 등 유사한 래퍼 함수가 난립하는 스파게티 상태였습니다.
impl은 API 호출만, 토큰은 OkHttp 레이어가 전담하도록 수정하였습니다.

주요 변경 사항

🏗 아키텍처

  • ApiError sealed class 도입 (core) — 서버/네트워크/비즈니스 에러 통합
  • ApiErrorMapper — 서버 에러코드 → ApiError 변환
  • safeApiCall / safeApiCallUnit / safeApiCall204 래퍼 함수로 통일
  • @AuthClient / @PublicClient Qualifier 분리
  • TokenAuthenticator — 401 시 자동 토큰 재발급 + Mutex (동시 401 방지)
  • ServerApiModule — PublicClient / AuthClient OkHttpClient 분리

🔧 RepositoryImpl authPreference 제거

파일 변경 내용
FolderRepositoryImpl withAuthsafeApiCall 교체, authPreference 제거
AIArticleRepositoryImpl withAuthsafeApiCall 교체, authPreference 제거
CurationRepositoryImpl authPreference 제거
LinkuRepositoryImpl withAuthsafeApiCall 교체, authPreference 제거
CategoryRepositoryImpl withAuthsafeApiCall 교체, authPreference 제거
AuthRepositoryImpl safeApiCall 적용, authPreference 제거
UserRepositoryImpl withAuthsafeApiCall 교체, authPreferencelogout()에서만 유지

📦 Module 정리

  • FolderRepositoryModule@Provides@Binds 전환
  • AuthRepositoryModule 신규 생성
  • RecentSearchRepositoryModule@RecentSearchDataStore Qualifier 추가 (DataStore 충돌 방지)

변경 전 / 후

// Before — 각 impl이 토큰 직접 관리
val dto = serverApi.withAuth(authPreference) {
    getUserInfo()
}

// After — impl은 호출만, 토큰은 OkHttp가 자동 처리
val dto = safeApiCall { serverApi.getUserInfo() }

미완료 (다음 PR)

  • 자동 로그인 리팩토링 (tryAutoLogin 토큰 재발급 로직)
  • ServerApiExt.kt @Deprecated 함수 완전 제거 (팀원 마이그레이션 완료 후)

리뷰 포인트

  • UserRepositoryImpl.logout()authPreference.clear() 위치 유지 여부
  • safeApiCall 에러 처리 방식 통일성 확인
  • BaseViewModel 구현 (공통 에러 처리) 방향성 의견 부탁드립니다

✔️ PR 유형

어떤 변경 사항이 있나요?

  • 새로운 기능 추가
  • 버그 수정
  • CSS 등 사용자 UI 디자인 변경
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 테스트 추가, 테스트 리팩토링
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📎 관련 이슈 번호

#118

Summary by CodeRabbit

  • 새로운 기능

    • 네트워크 연결 상태 자동 감지 및 연결 끊김 안내 토스트 표시
    • 기기 정보(디바이스 ID/타입) 최초 저장으로 세션 복원 개선
    • 백 버튼 이중 누름으로 앱 종료 처리 추가
  • 개선사항

    • 인증·토큰 재발급 로직 강화로 로그인 신뢰성 향상
    • 이메일 인증/코드 전송·검증 흐름 및 화면 분리로 UX 개선
    • 약관 동의 흐름(상태/이벤트 기반) 및 약관 화면 사용성 개선
    • 소셜 로그인 동작 보완 및 자동 로그인 처리 정비

Review Change Stack

- `StepIndicator`에서 불필요한 `totalSteps` 및 `label` 파라미터를 제거하고 내부 `stepLabel` 함수로 대체
- `StepIndicator`의 단계별(1~3단계) 원 크기, 배경색, 아이콘 표시 로직을 `when` 식을 사용하여 명시적으로 재구현
- `SignUpStepLayout` 및 `SignUpSelectionLayout`에서 사용되지 않는 `totalSteps`, `label`, `stepLabel` 프로퍼티 삭제
- 이메일 및 소셜 회원가입 관련 모든 화면(`EmailInputScreen`, `SignUpPasswordScreen`, `SocialNicknameScreen` 등)의 레이아웃 호출부 업데이트
- `EmailVerificationScreen`에서 사용되지 않는 `timer` 상태 변수 제거
- `FolderApi` 및 `_ShareBottomSheet`에서 미사용 코드 주석 처리 및 임포트 정리
- `StepIndicator` 프리뷰 코드를 단일 컴포ザ블로 통합 및 간소화
- `TermsAgreementState` 및 `TermsAgreementEvent` 데이터 모델을 추가하여 약관 동의 관련 상태와 이벤트를 캡슐화
- `TermsAgreementSheet` 및 `TermsAgreementContent`가 `ViewModel` 대신 위 모델을 직접 참조하도록 수정하여 UI 결합도 해제
- `NoAnimBottomSheet`에서 불필요한 파라미터(`visible`, `scrimColor`, `shape`)를 제거하고 내부 구현 단순화
- `ServiceTermsScreen`, `PrivacyTermsScreen`, `MarketingTermsScreen`에 `alreadyAgreed` 파라미터를 추가하여 이미 동의한 경우 하단 버튼이 바로 활성화되도록 로직 개선
- `LoginTextField` 및 `PasswordLoginTextField`에서 불필요한 `textStyle` 파라미터를 제거하고 `modifier` 위치 등 파라미터 순서 정리
- `LoginApp` 내비게이션 그래프에서 `TermsAgreementSheet` 호출 시 상태 및 이벤트 매핑 로직 업데이트
- 불필요한 import 문 및 사용되지 않는 변수/리소스 제거 (HorizontalDivider 사용 등)
- 이메일 인증 코드 전송(`sendVerificationEmail`) 및 검증(`checkVerificationEmail`) API의 파라미터 전달 방식을 `@Query`에서 `@Body`로 변경
- 이메일 인증 관련 DTO 클래스 추가 (`EmailCodeRequestDTO`, `EmailVerifyRequestDTO`)
- `AuthRepositoryImpl`에서 변경된 API 명세에 맞춰 요청 로직 수정
- `EmailVerificationScreen` 진입 시 타이머 상태를 유지하기 위해 `viewModel.resetAll()`을 `viewModel.reset()`으로 변경
====
♻️ refactor(login): 이메일 인증 화면 상태 관리 구조 개선 및 리팩터링

- `EmailVerificationUiState` 및 `EmailVerificationEvents` 데이터 클래스를 추가하여 UI 상태와 이벤트 핸들러를 캡슐화
- `EmailVerificationScreenContent`가 개별 파라미터 대신 위 모델들을 참조하도록 수정하여 가독성 및 유지보수성 향상
- `EmailAuthViewModel`에서 관리하는 `isCodeSent`, `timer` 상태를 `collectAsStateWithLifecycle`로 구독하도록 변경
- 이메일 입력(1단계)과 인증 코드 입력(2단계) UI 로직을 `when` 조건을 통해 명확히 분리
- `TimerText` 컴포넌트에서 `ViewModel` 의존성을 제거하고 `timer` 값을 직접 주입받도록 수정
- 불필요한 import 및 주석을 제거하고 프리뷰 데이터를 신규 UI 모델 구조에 맞게 업데이트
====
- 이메일 인증 코드 발송 요청을 위한 `SendEmailCodeRequestDTO` 추가
- 이메일 인증 코드 확인 요청을 위한 `CheckEmailCodeRequestDTO` 추가 (email, code 필드 포함)
- Moshi 기반의 JSON 직렬화를 위한 `@JsonClass` 및 `@Json` 어노테이션 적용
- `AndroidManifest.xml`에 네트워크 상태 접근 권한(`ACCESS_NETWORK_STATE`) 추가
- `MainViewModel`을 `AndroidViewModel`로 변경하고 `ConnectivityManager`를 이용한 실시간 네트워크 감지 로직 구현
- `callbackFlow`를 사용하여 네트워크 가용 상태를 `StateFlow<Boolean>` 형태로 제공 (`isConnected`)
- `MainApp` 컴포넌트에서 `isConnected` 상태를 구독하여 네트워크 미연결 시 토스트 메시지 알림 표시
- `MainViewModel` 생성자 변경에 따른 의존성 주입(Hilt) 및 관련 코드 정리
- `core` 모듈에 서버 및 클라이언트 에러를 계층화한 `ApiError` sealed class 정의
    - `Common` (400, 401, 403, 429, 500 등 HTTP 상태 코드 기반)
    - `Network` (연결 없음, 타임아웃)
    - 도메인별 에러: `Auth`, `User`, `S3`, `Linku`, `Resource`, `Folder`, `Alarm` 등 세부 에러 코드 반영
- `data` 모듈에 Retrofit 호출을 위한 공통 처리 로직 및 유틸리티 추가
    - `safeApiCall`, `safeApiCallUnit`: API 응답의 성공 여부를 판단하고 예외 발생 시 `ApiError`로 변환하여 throw하는 래퍼 함수 구현
    - `AuthClient`, `PublicClient`: Dagger/Hilt용 API 클라이언트 구분 어노테이션(Qualifier) 추가
- `HttpException`, `IOException` 등 네트워크 예외 상황에 대한 에러 매핑 로직 구현 (단, `mapToApiError` 및 `mapHttpError` 구현체는 외부 참조로 포함됨)
- `AuthApi` 및 `AuthRepository`의 API 호출 방식을 `safeApiCall` 및 `safeApiCallUnit` 공통 모듈 기반으로 리팩터링
- 토큰 재발급(`reissue`) 시 `deviceId`를 포함하도록 `ReissueRequestDTO`를 적용하고 로직 수정
- `AuthPreference`에 소셜 로그인을 위한 `socialToken` 관리 필드 추가 및 `refreshToken` 널 허용 처리
- 소셜 프로필 완성 API 엔드포인트를 `/auth/signup/social/profile`에서 `/auth/signup/social/complete`로 변경
- 구글 로그인 인터페이스의 반환 타입을 `Result<LoginResult>`로 변경하여 에러 핸들링 강화
- `AuthRepositoryImpl` 내 로깅 메시지 정리 및 불필요한 주석 제거
- 이메일 인증 및 닉네임 중복 확인 등 결과값이 없는 API의 반환 타입을 `Any` 또는 `Unit`으로 통일하여 처리 로직 간소화
- 서버 에러 코드(String)를 도메인 에러 객체(`ApiError`)로 변환하는 `mapToApiError` 함수 추가
- COMMON, OAUTH, USERS, S3, LINKU, FOLDER, ALARM 등 도메인별 상세 에러 코드 대응 로직 구현
- `HttpException`을 기반으로 일반적인 HTTP 상태 코드를 매핑하는 `mapHttpError` 함수 추가
- 기존 `ServerApiExt.kt` 내 `ApiError` 클래스에 `@Deprecated` 어노테이션 추가 및 미사용 토큰 갱신 로직 주석 처리 (신규 `safeApiCall` 구조로 전환 준비)
…구조 개선

- `TokenAuthenticator` 클래스를 추가하여 401 Unauthorized 발생 시 `refreshToken`을 이용한 토큰 재발급 및 요청 재시도 로직 구현
- `ServerApiModule` 리팩터링을 통해 토큰 필요 여부에 따른 Retrofit 인스턴스 분리 (`@AuthClient`, `@PublicClient` 한정자 도입)
- `AuthApi`를 `ServerApi` 상속 구조에서 분리하여 토큰이 불필요한 공용 API로 독립 (로그인, 회원가입, 재발급 등)
- 토큰 재발급 요청을 위한 `ReissueRequestDTO` 데이터 클래스 추가
- 네트워크 인터셉터에서 불필요한 경로 체크 로직을 제거하고, `Authenticator`와 `addNetworkInterceptor`를 통한 표준 인증 방식으로 전환
- `UserIdNullException`의 사소한 코드 포맷 수정 (기본 파라미터 공백 정리)
- `LinkuRepositoryModule`, `CategoryRepositoryModule`, `FolderRepositoryModule`, `AIArticleRepositoryModule`을 `object`에서 `abstract class`로 변경하고 `@Provides` 대신 `@Binds`를 사용하도록 리팩터링
- `AuthRepositoryModule`을 신규 생성하여 `RepositoryModule`에 있던 `AuthRepository` 바인딩 로직을 분리 및 이동
- `CurationRepositoryImpl` 생성 시 더 이상 사용되지 않는 `authPreference` 파라미터 제거
- DI 바인딩 메서드에 `@Suppress("unused")` 어노테이션을 추가하여 정적 분석 경고 대응
- 불필요한 import 문(`ServerApi`, `AuthPreference` 등) 정리 및 코드 가독성 개선
- `Response<Unit>`을 반환하는 API 호출(예: 데이터 삭제)을 안전하게 처리하기 위한 래퍼 함수 `safeApiCall204` 구현
- 성공적인 응답(2xx) 시 결과값 없이 반환하며, 실패 시 `HttpException`을 `ApiError`로 매핑하여 throw 하도록 처리
- 네트워크 연결 오류, 타임아웃, 일반 예외 상황에 대한 예외 처리 로직 포함 (기존 `safeApiCall`의 예외 처리 구조와 동일)
…ApiCall`로 교체

- 모든 Repository 클래스(`Folder`, `AIArticle`, `Linku`, `Category`, `Auth`)에서 사용되지 않는 `AuthPreference` 주입 제거
- 확장 함수 기반의 인증 처리 방식(`withAuth`, `withAuthResp204Raw`)을 표준화된 `safeApiCall` 및 `safeApiCall204` 유틸리티 함수 사용으로 전환
- API 호출 시 `serverApi` 인스턴스를 `safeApiCall` 블록 내부에서 직접 참조하도록 구조 변경
- `CurationRepositoryImpl`에 향후 연동을 위한 주석 추가
- `AuthRepositoryImpl` 내 불필요하게 선언된 `signUpEmailRequest` 지역 변수 제거 및 로직 최적화
…조 개선

- `DataStore<Preferences>`의 명확한 주입을 위해 `@RecentSearchDataStore` 한정자 어노테이션 정의
- `RecentSearchRepositoryModule`에서 `provideRecentSearchDataStore`와 `provideRecentSearchRepository`에 해당 한정자를 적용하여 의존성 충돌 방지 및 명확성 확보
- `LoginSessionStore` 내 기기 식별 정보(`DEVICE_ID`, `DEVICE_TYPE`) 저장을 위한 Key 정의
- 기기 정보를 최초 1회만 저장하는 `saveDeviceInfoIfAbsent()` 함수 및 읽기용 Flow(`deviceId`, `deviceType`) 추가
- `clear()` 수행 시 세션 정보만 삭제하고 기기 정보는 유지되도록 로직 수정
- `LoginApp` 컴포넌트에서 사용되지 않는 `showNavBar` 파라미터 및 관련 `LaunchedEffect` 제거
- `LoginSessionStore` 내 주석 정리 및 코드 가독성 개선 (Long 타입 필드 등)
- **기능 개선 및 리팩터링**
  - `DoubleBackToExitIfTop` 컴포넌트를 적용하여 중복 구현된 뒤로가기 종료 로직 제거 및 관리 최적화
  - `MainApp`에서 사용하지 않는 `BackHandler`, `activity` 참조 및 관련 변수(`lastBackPressed`) 주석 처리
  - `Splash` 및 로그인 성공 로직에 자동 로그인 및 세션 갱신 리팩터링을 위한 TODO 추가
  - `MainApplication`에서 디버그용 `keyHash` 확인 로직 및 불필요한 import(`Utility`) 제거

- **내비게이션 및 UI**
  - 로그인 화면(`login_root`) 진입 시 바텀 네비게이션 바를 숨기도록 명시적으로 설정
  - 딥링크 관련 미사용 로그인 라우트(`NavigationRoute.Login.route`) 및 관련 모달 로직 제거
  - `MainActivity`에 세션 갱신 로직 추가를 위한 `onResume` 오버라이드 추가

- **기타**
  - 앱 실행 시 `clearRecentQuery()` 호출 위치 유지 및 관련 개선 사항 주석 추가
  - 불필요한 import 정리 및 `hiltViewModel` 패키지 경로 최적화 (`androidx.hilt.lifecycle.viewmodel.compose`)
- 더 이상 사용되지 않는 `GsonModule` 삭제
- `LocalDate` 및 `LocalDateTime`에 대한 커스텀 직렬화 설정(`LocalDateSerializer`, `LocalDateTimeSerializer`) 제거
- `Hilt` 의존성 주입 범위(`SingletonComponent`)에서 `Gson` 객체 제공 중단
- **소셜 로그인 (카카오/구글) 처리 개선**
    - `refreshToken` 유무에 따라 신규(TEMP) 유저와 기존 유저를 구분하는 로직 추가
    - 신규 유저의 경우 토큰을 영구 저장하지 않고 `authPreference.socialToken`에 임시 보관하도록 수정
    - 구글 로그인의 상태 관리를 위한 `GoogleLoginStatus` 인터페이스 및 `work()` 메서드 구조 도입
    - `loginWithGoogle` 함수를 `suspend`로 변경하고 `Result` 타입을 통한 에러 핸들링 반영

- **기기 정보(DeviceId, DeviceType) 관리 로직 이관**
    - `LoginViewModel` 내부에 직접 구현되어 있던 `deviceId` 및 `getDeviceType()` 로직 제거
    - `loginSessionStore`를 통해 저장된 기기 정보를 비동기(`first()`)로 가져오도록 수정
    - 저장된 기기 정보가 없을 경우 `UUID`를 생성하고 `loginSessionStore`에 저장하는 초기화 로직 추가

- **기타 수정 사항**
    - `LoginViewModel` 내 불필요한 주석 및 `Settings.Secure` 관련 import 제거
    - `SocialAuthViewModel` 내 로깅 및 미사용 코드 정리 (주석 처리 등)
…ticator` 갱신 로직 보완

- `UserRepositoryImpl.getUserInfo` 호출 시 기존 `withAuth` 래퍼 대신 `safeApiCall`을 사용하도록 변경 및 불필요한 파라미터 주석 처리
- `TokenAuthenticator`에서 토큰 재발급 시, 이미 다른 요청에 의해 토큰이 갱신되었는지 확인하는 로직 추가 (중복 갱신 방지)
- `TokenAuthenticator` 내 `runBlocking` 블록의 반환 제어 흐름을 `withLock` 컨텍스트에 맞게 수정
- `UserRepositoryImpl` 내 사용되지 않는 import 정리 및 향후 리팩터링을 위한 주석 추가
# Conflicts:
#	app/src/main/AndroidManifest.xml
#	app/src/main/java/com/linku/MainApp.kt
#	app/src/main/java/com/linku/MainViewModel.kt
- `MainViewModel`의 상속 클래스를 `ViewModel`에서 `AndroidViewModel`로 변경하여 `Application` 컨텍스트 활용 가능하도록 수정
- 생성자 파라미터에 `NotificationController`를 추가하여 의존성 주입 로직 반영
- 파일 하단에 중복으로 포함되어 있던 잘못된 코드 블록(중복 생성자 정의 및 필드 선언) 제거
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Walkthrough

에러 계층 추가, 안전 API 래퍼(safeApiCall 계열), TokenAuthenticator/Retrofit 분리, 네트워크 상태 감지(isConnected), LoginSessionStore의 device 보존, 인증 리포지토리·DI 리팩터, 약관·이메일 UI 상태/이벤트 분리 등 인증·네트워크 관련 통합 개편입니다.

Changes

인증 및 네트워크 감지 기반 개편

Layer / File(s) Summary
에러 계층 및 API 안전 래퍼
core/src/main/java/com/linku/core/error/ApiError.kt, data/src/main/java/com/linku/data/api/ApiExt.kt, data/src/main/java/com/linku/data/api/mapToApiError.kt
도메인별 ApiError 계층 정의(공통/네트워크/Auth/User/Folder/AI 등) 및 문자열/HTTP 상태 코드 매핑, safeApiCall/safeApiCallUnit/safeApiCall204로 응답 검증·예외 매핑 구현.
API 인프라 및 토큰 재발급
data/src/main/java/com/linku/data/api/AuthQualifiers.kt, data/src/main/java/com/linku/data/api/AuthApi.kt, data/src/main/java/com/linku/data/api/TokenAuthenticator.kt, data/src/main/java/com/linku/data/di/api/ServerApiModule.kt, data/src/main/java/com/linku/data/api/dto/auth/refreshToken/ReissueRequestDTO.kt
@PublicClient/@AuthClient로 Retrofit 분리, TokenAuthenticator 추가해 401 시 재발급·재시도 구현, ReissueRequestDTO 도입, AuthApi 시그니처 정리.
ServerApiExt 변형
data/src/main/java/com/linku/data/api/ServerApiExt.kt
기존 refreshTokenIfNeeded 구현과 withTokenRefresh 내부 재발급 호출 일부가 주석 처리되어 자동 재발급 경로의 일부가 비활성화됨; ApiError 선언에 Deprecated 주석 추가.
네트워크 연결 상태 추적
app/src/main/java/com/linku/MainViewModel.kt, app/src/main/AndroidManifest.xml
MainViewModel이 AndroidViewModel로 전환되어 ConnectivityManager 접근, NetworkCallback을 callbackFlow로 감싸 isConnected: StateFlow 제공; AndroidManifest에 ACCESS_NETWORK_STATE 권한 추가.
기기 정보 저장 및 세션 관리
core/src/main/java/com/linku/core/datastore/session/LoginSessionStore.kt
DEVICE_ID/DEVICE_TYPE 키 추가, saveDeviceInfoIfAbsent() 도입, deviceId/deviceType Flow 노출, clear() 동작에서 기기 정보 보존 후 세션 데이터만 삭제하도록 변경.
AuthPreference 변경
data/src/main/java/com/linku/data/preference/AuthPreference.kt, data/src/main/java/com/linku/data/implementation/preference/AuthPreferenceImpl.kt
refreshToken을 nullable로 변경하고 socialToken: String? 추가, AuthPreferenceImpl에 socialToken 저장/삭제 구현 및 clear()에 social token 제거 추가.
인증 리포지토리 재설계
data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt, core/src/main/java/com/linku/core/repository/AuthRepository.kt
AuthRepository 계약을 Result로 래핑하고 구현을 AuthApi + safeApiCall 계열로 전환하여 이메일/소셜 로그인, 재발급, 프로필 완료 흐름을 재구성.
리포지토리 전반 safeApiCall 전환
data/.../implementation/repository/*Impl.kt (Folder, Linku, Category, AIArticle, User 등)
다수 리포지토리에서 withAuth/AuthPreference 직접 사용을 제거하고 safeApiCall/safeApiCall204로 호출 패턴을 통일함(관련 생성자 주입 정리 포함).
DI 모듈 @Provides@Binds 전환
data/src/main/java/com/linku/data/di/repository/*RepositoryModule.kt, data/src/main/java/com/linku/data/di/qualifier/DataStoreQualifiers.kt
여러 RepositoryModule을 object/@provides에서 abstract/@BINDS로 전환, AuthRepositoryModule 추가, RecentSearchDataStore qualifier 도입 등으로 DI 구성 정리.
MainApp 및 네비게이션·뒤로가기 처리
app/src/main/java/com/linku/MainApp.kt, app/src/main/java/com/linku/MainActivity.kt, app/src/main/java/com/linku/navigation/DoubleBackToExitIfTop.kt, feature/login/src/main/java/com/linku/login/navigation/LoginApp.kt
LoginApp의 showNavBar 콜백 제거·LaunchedEffect 사용, MainApp에 네트워크 단절 토스트 추가, 뒤로가기 종료 로직을 DoubleBackToExitIfTop으로 통합, 로그인 라우트·딥링크 성공 처리 흐름 재배치.
약관 동의 UI 상태/이벤트 패턴 도입
feature/login/src/main/java/com/linku/login/ui/model/TermsAgreementState.kt, feature/login/src/main/java/com/linku/login/ui/model/TermsAgreementEvent.kt, feature/login/src/main/java/com/linku/login/ui/bottom_sheet/TermsAgreementSheet.kt, feature/login/src/main/java/com/linku/login/ui/content/TermsAgreementContent.kt, feature/login/src/main/java/com/linku/login/ui/terms/*
TermsAgreementState/Event로 상태·이벤트 분리, TermsAgreementSheet/Content 리팩터링, NoAnimBottomSheet API 단순화, 상세 약관에 alreadyAgreed 파라미터 추가.
이메일 인증 UI 상태 분리 및 타이머 개선
feature/login/src/main/java/com/linku/login/ui/model/EmailVerificationUiState.kt, feature/login/src/main/java/com/linku/login/ui/model/EmailVerificationEvents.kt, feature/login/src/main/java/com/linku/login/ui/screen/email/EmailVerificationScreen.kt
EmailVerificationUiState/Events로 상태와 이벤트 분리, Content 컴포저블의 API 축소 및 타이머 파라미터화로 프리젠테이션 단순화.
UI 컴포넌트 단순화 및 파라미터 정리
여러 feature/login/src/main/java/com/linku/login/ui/...
StepIndicatorcurrentStep만 받도록 축소(3단계 고정), LoginTextField/PasswordLoginTextField 파라미터 정리, SignUpStepLayout/SignUpSelectionLayout에서 totalSteps/label 제거 등.
소셜 로그인 강화 및 토큰 조건부 저장
feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt
Kakao 로그인에서 refreshToken 유무에 따른 저장 분기(없으면 socialToken만 저장), Google 로그인 비동기(suspend) 전환 및 GoogleLoginStatus 도입, Result 기반 처리.

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ugmin1030
  • Hongji03
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/110

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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
core/src/main/java/com/linku/core/repository/AuthRepository.kt (1)

50-58: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

loginWithKakaologinWithGoogle의 반환 타입이 일관되지 않습니다.

loginWithKakaoLoginResult를 직접 반환하고, loginWithGoogleResult<LoginResult>를 반환합니다. 호출자 입장에서 동일한 소셜 로그인 기능인데 에러 처리 방식이 다르면 혼란스럽습니다.

두 메서드 모두 동일한 반환 타입을 사용하도록 통일하는 것이 좋습니다:

  • 둘 다 Result<LoginResult>로 통일하거나
  • 둘 다 LoginResult로 반환하고 예외로 에러 처리
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/main/java/com/linku/core/repository/AuthRepository.kt` around lines
50 - 58, The two methods loginWithKakao and loginWithGoogle return inconsistent
types (LoginResult vs Result<LoginResult>); make them consistent by choosing one
approach (preferably both return Result<LoginResult> for unified error handling)
and update the function signatures for loginWithKakao and loginWithGoogle to
match, then adjust their implementations, callers, and any tests to handle
Result<LoginResult> (or, if you choose exceptions, change both to return
LoginResult and propagate/handle exceptions consistently). Ensure the
service/repository interface and any implementing classes reference the same
return type and that imports for Result and LoginResult are correct.
data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt (1)

485-510: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

updateLinkFolder가 API 응답 대신 원래 입력값을 반환합니다.

result 변수에 API 응답을 매핑했지만 사용하지 않고, 원래 입력 파라미터인 linku를 그대로 반환합니다. 서버 측 변경 사항(예: 업데이트된 타임스탬프, 서버에서 수정된 값 등)이 호출자에게 전달되지 않습니다.

🐛 수정 제안
-            Log.d("FolderRepositoryImpl", "updateLinkFolder response: $result")
-        } catch (e: Exception) {
-            Log.d("FolderRepositoryImpl", "updateLinkFolder error: $e")
-            throw e
-        }
-
-        Log.d("FolderRepositoryImpl", "updateLinkFolder return: $linku")
-        return linku
+            Log.d("FolderRepositoryImpl", "updateLinkFolder response: $result")
+            Log.d("FolderRepositoryImpl", "updateLinkFolder return: $result")
+            return result
+        } catch (e: Exception) {
+            Log.d("FolderRepositoryImpl", "updateLinkFolder error: $e")
+            throw e
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`
around lines 485 - 510, The method updateLinkFolder currently maps the API
response into result (using safeApiCall -> serverApi.updateLinkFolder and
constructing a LinkItemInfo) but then returns the original linku parameter
instead of the mapped result; fix this in updateLinkFolder by returning the
constructed LinkItemInfo (or assigning result to linku before return) so callers
receive the server-updated values (use the result from the safeApiCall mapping),
and ensure the mapping code that builds LinkItemInfo (fields linkuId,
parentFolderId, title, url/domain, linkuImageUrl, createdAt) is used as the
method return value.
data/src/main/java/com/linku/data/api/ServerApiExt.kt (1)

135-160: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

401 분기에서 실제 토큰 재발급 호출이 제거되어 복구가 불가능합니다.

Line 158에서 재발급 호출이 빠져 현재는 만료 토큰으로 동일 요청만 재시도합니다. 이 확장 함수를 아직 사용하는 경로에서는 401 복구가 깨집니다.

수정 예시
-//private suspend fun ServerApi.refreshTokenIfNeeded(
-//    authPreference: AuthPreference
-//) = refreshMutex.withLock { //락 걸음.
-//    val refresh = authPreference.refreshToken
-//        ?: throw ApiError.TokenExpired("다시 로그인해주세요")
-//    //헤더 방식으로 변환.
-//    val pair = withCheck { reissue(refresh) }
-//    pair.refreshToken?.let { authPreference.refreshToken = it }
-//    pair.accessToken?.let { authPreference.accessToken = it } //null이 아니면 실행
-//}
+private suspend fun ServerApi.refreshTokenIfNeeded(
+    authPreference: AuthPreference
+) = refreshMutex.withLock {
+    val refresh = authPreference.refreshToken
+        ?: throw ApiError.TokenExpired("다시 로그인해주세요")
+    val pair = withCheck { reissue(refresh) }
+    pair.refreshToken?.let { authPreference.refreshToken = it }
+    pair.accessToken?.let { authPreference.accessToken = it }
+}
@@
-//                refreshTokenIfNeeded(authPreference)
+                refreshTokenIfNeeded(authPreference)
                 block() // api 재시도
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/src/main/java/com/linku/data/api/ServerApiExt.kt` around lines 135 -
160, The 401 recovery currently skips the actual token reissue, so inside
ServerApi.withTokenRefresh (the catch where shouldRefreshToken(exception) is
true) call the token refresh routine (refreshTokenIfNeeded(authPreference))
before retrying block(); use the existing refreshTokenIfNeeded(authPreference)
helper (which handles refreshMutex and updates
AuthPreference.refreshToken/accessToken) and keep the retry block() and the
outer error handling/propagation intact so that failed refreshes still throw the
refreshError.
app/src/main/java/com/linku/MainApp.kt (2)

258-307: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

autoLoginTried 가드가 현재는 절대 켜지지 않습니다.

Line 276에서 재시도를 막으려 하지만, 이 파일에서는 autoLoginTriedtrue로 바꾸는 코드가 없습니다. 지금 상태로는 가드 분기가 영원히 닿지 않아서, Splash가 다시 onResult를 호출하는 경우 tryAutoLogin()이 그대로 재실행됩니다.

🔧 제안 수정
                                 if (!hasRefresh) {
                                     // refresh 없음 → 로그인 화면으로 이동
                                     navigator.navigate("login_root") {
                                         popUpTo(NavigationRoute.Splash.route) { inclusive = true }
                                     }

                                     return@Splash
                                 }

-
-                                // refresh 있음 → 자동로그인 시도
+                                // refresh 있음 → 자동로그인 시도
+                                autoLoginTried = true
                                 loginViewModel.tryAutoLogin(
                                     onSuccess = {
                                         navigator.navigate(NavigationRoute.Home.route) {
                                             popUpTo(NavigationRoute.Splash.route) { inclusive = true }
                                             launchSingleTop = true

As per coding guidelines: Focus on state management (state hoisting, remember vs rememberSaveable) in Android/Kotlin/Compose code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/linku/MainApp.kt` around lines 258 - 307,
autoLoginTried is never set true so the retry-guard never triggers; update state
handling so autoLoginTried is toggled when an auto-login attempt starts (or
immediately in onResult before calling loginViewModel.tryAutoLogin) and/or when
the attempt completes (in onSuccess and onFail) to prevent repeated retries;
reference the variables/functions Splash, onResult, autoLoginTried, and
loginViewModel.tryAutoLogin and either set autoLoginTried = true right before
invoking tryAutoLogin or hoist this flag into LoginViewModel (preferred) so the
boolean is updated reliably across recompositions and restored appropriately (if
persisted use rememberSaveable, otherwise remember/VM state).

115-127: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

로그인 성공 네비게이션을 여기서 한 번 더 수행하면 충돌합니다.

LaunchedEffect(loginState)는 Line 295의 스플래시 자동 로그인 성공 처리와 Line 318의 onLoginSuccess가 이미 담당하는 이동을 다시 실행합니다. 수동 로그인에서는 pending share를 파일 화면으로 보내기 전에 홈으로 popUpTo 해버릴 수 있고, 성공 네비게이션도 중복 호출됩니다. 로그인 후 이동은 한 경로에서만 처리하고, 공통 후처리만 별도 helper로 분리하는 편이 안전합니다.

As per coding guidelines: Focus on SideEffect / LaunchedEffect misuse in Android/Kotlin/Compose code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/linku/MainApp.kt` around lines 115 - 127, This
LaunchedEffect(loginState) is duplicating navigation already handled elsewhere
(splash auto-login and onLoginSuccess); remove the navigator.navigate(...) block
from this LaunchedEffect and factor only the shared post-login work into a
helper (e.g., handlePostLogin or loginViewModel.handlePostLogin) that performs
homeViewModel.refreshAfterLogin() and sets showNavBar = true, then call that
helper from the single canonical navigation sites (onLoginSuccess and the splash
auto-login handler) so navigation
(navigator.navigate(NavigationRoute.Home.route) / popUpTo / launchSingleTop) is
executed only once.
🧹 Nitpick comments (9)
data/src/main/java/com/linku/data/di/repository/LinkuRepositoryModule.kt (1)

19-21: ⚡ Quick win

바인딩 함수명과 대상 리포지토리명을 맞춰 주세요.

LinkuRepository를 바인딩하는 메서드가 bindFolderRepository로 되어 있어 의도가 혼동됩니다. 메서드명을 맞추는 게 좋습니다.

제안 diff
-    abstract fun bindFolderRepository(
+    abstract fun bindLinkuRepository(
         impl: LinkuRepositoryImpl
     ): LinkuRepository
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/src/main/java/com/linku/data/di/repository/LinkuRepositoryModule.kt`
around lines 19 - 21, 메서드명과 바인딩 대상이 불일치합니다: 현재 bindFolderRepository가
LinkuRepositoryImpl을 LinkuRepository에 바인딩하고 있어 혼동을 유발하므로 bindFolderRepository의
이름을 LinkuRepository로 바인딩하는 의도에 맞게 변경하세요; 구체적으로 LinkuRepositoryModule에서 함수명
bindFolderRepository를 LinkuRepository(또는 bindLinkuRepository)로 바꾸어
LinkuRepositoryImpl -> LinkuRepository 바인딩을 명확히 하세요.
data/src/main/java/com/linku/data/di/repository/CurationRepositoryModule.kt (1)

15-37: ⚡ Quick win

주석 처리된 이전 구현과 임시 메모는 정리해 주세요.

현재 파일에 과거 구현 블록과 리뷰성 코멘트가 함께 남아 있어 DI 설정 의도를 읽기 어렵습니다. 머지 전에 삭제하는 편이 유지보수에 유리합니다.

제안 diff
-/* 미래의 제가 수정할겁니다. */
 `@Module`
 `@InstallIn`(SingletonComponent::class)
 object CurationRepositoryModule {
-
-
-//    `@Provides`
-//    `@Singleton`
-//    fun provideLinkuRepository(
-//        serverApi: ServerApi,
-//        authPreference: AuthPreference
-//    ): CurationRepository {
-//        return CurationRepositoryImpl(
-//            serverApi = serverApi,
-//            authPreference = authPreference
-//        )
-//    }
 `@Provides`
 `@Singleton`
-fun provideCurationRepository(                // 함수명도 의미 맞게 변경 권장
+fun provideCurationRepository(
     serverApi: ServerApi,
-    curationApi: CurationApi,                 // ★ 추가
+    curationApi: CurationApi,
     authPreference: AuthPreference,
     moshi: Moshi
 ): CurationRepository {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/src/main/java/com/linku/data/di/repository/CurationRepositoryModule.kt`
around lines 15 - 37, Remove the leftover commented-out implementation and
inline memo from CurationRepositoryModule: delete the commented
provideLinkuRepository block and the Korean temporary note ("/* 미래의 제가 수정할겁니다.
*/") so the module only contains the active DI provider
(provideCurationRepository) and related imports; ensure no stray review comments
remain and keep function names (CurationRepositoryModule,
provideCurationRepository) and added parameter curationApi intact.
data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt (2)

245-273: ⚡ Quick win

주석 처리된 코드를 제거해주세요.

더 이상 사용되지 않는 구글 로그인 코드가 주석으로 남아있습니다. 버전 관리를 통해 이전 코드를 확인할 수 있으므로, 코드베이스 정리를 위해 삭제하는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt`
around lines 245 - 273, Remove the dead commented Google login block: delete the
entire commented-out loginWithGoogle function and its related commented
imports/usages (the block referencing loginWithGoogle, SocialLoginResponseDTO,
SocialLoginRequestDTO, and serverApi.withErrorHandling) so the file no longer
contains unused commented code; rely on VCS to retain history if needed.

210-216: 💤 Low value

inactiveDate 처리가 이메일 로그인과 일관되지 않습니다.

이메일 로그인(line 78)에서는 emailSignUpResponse.inactiveDate를 사용하지만, 카카오 로그인에서는 항상 빈 문자열 ""을 할당합니다. 소셜 로그인 응답에도 inactiveDate 필드가 있다면 해당 값을 사용하거나, 없다면 null을 사용하는 것이 일관성 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt`
around lines 210 - 216, The Kakao login branch constructs a LoginResult with
inactiveDate set to an empty string, which is inconsistent with the email
sign-up branch that uses emailSignUpResponse.inactiveDate; update the Kakao flow
to read the actual inactiveDate from the social response (e.g.,
response.inactiveDate) if present, or set it to null when the API doesn't
provide it, instead of "" so LoginResult.inactiveDate usage is consistent across
flows; adjust any nullable types or LoginResult constructor if needed to accept
null.
data/src/main/java/com/linku/data/implementation/preference/AuthPreferenceImpl.kt (1)

89-89: 💤 Low value

TODO 주석을 명확히 해주세요.

//TODO : 수정하기가 무엇을 수정해야 하는지 불분명합니다. 이 PR에서 처리할 내용인지, 아니면 후속 작업인지 명확히 해주세요. TODO가 남아있어야 한다면 구체적인 내용을 기술해주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/preference/AuthPreferenceImpl.kt`
at line 89, The comment on the refreshToken parameter in AuthPreferenceImpl.kt
is ambiguous; replace "//TODO : 수정하기." with a clear, actionable note: either
resolve the TODO now (describe the intended change, e.g., "make refreshToken
non-nullable once migration completes" or "store refresh token encrypted using X
method") or mark it as a tracked follow-up with an issue ID and expected
owner/ETA (e.g., "TODO: revisit nullability & encryption — see ISSUE-123
assigned to `@owner` by yyyy-mm-dd"). Update the comment adjacent to the
refreshToken parameter so reviewers know whether this PR addresses it or it is
deferred and what exactly must be changed.
data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt (1)

461-465: 💤 Low value

주석 처리된 코드 블록을 제거하세요.

deleteSharedFolder 호출이 주석 처리되어 있습니다. 테스트/디버깅 목적으로 남겨진 것으로 보이며, 병합 전에 정리가 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`
around lines 461 - 465, 파일의 FolderRepositoryImpl 클래스에 남아 있는 주석 처리된 코드 블록
(safeApiCall204 { serverApi.deleteSharedFolder(folderId) })을 제거하세요; 해당 주석은
테스트용으로 보이므로 병합 전 삭제해야 하며, 관련된 메서드 이름 deleteSharedFolder 및 유틸 호출 safeApiCall204를
참고해 주석만 삭제하고 기능적 변경은 하지 마세요.
data/src/main/java/com/linku/data/implementation/repository/LinkuRepositoryImpl.kt (1)

137-144: 💤 Low value

safeApiCall이 항상 unwrap된 결과를 반환한다면 타입 체크가 불필요합니다.

safeApiCall이 이미 BaseResponse<T>에서 T를 추출해 반환한다면, when (raw) 블록의 BaseResponse<*> 케이스는 도달하지 않을 수 있습니다. safeApiCall의 반환 타입을 확인하고, 불필요하다면 단순화를 고려하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/LinkuRepositoryImpl.kt`
around lines 137 - 144, The when-check on raw is likely redundant because
safeApiCall already unwraps BaseResponse<T>; inspect safeApiCall's return type
and, if it returns the inner T for serverApi.recentLinks, simplify by removing
the BaseResponse<*> branch and directly map/convert raw (the result of
safeApiCall { serverApi.recentLinks(limit = limit) }) to List<LinkuSimpleDTO>
(e.g., filterIsInstance or a safe cast) so the code uses a single, correct path
for obtaining list: List<LinkuSimpleDTO>.
data/src/main/java/com/linku/data/implementation/repository/UserRepositoryImpl.kt (1)

108-140: 🏗️ Heavy lift

safeApiCall 마이그레이션이 일관성 없이 적용되었습니다.

getUserInfo()safeApiCall로 마이그레이션되었지만, updateUserInfo(), deleteUser(), getNickname()은 여전히 withAuth/withAuthRaw를 사용합니다. PR 목표에 따르면 authPreferencelogout()에만 유지되어야 하는데, 현재 여러 메서드에서 사용 중입니다.

의도적인 단계적 마이그레이션이라면 TODO 주석을 추가하거나, 이번 PR에서 함께 마이그레이션하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/UserRepositoryImpl.kt`
around lines 108 - 140, Several methods are inconsistent in migrating to
safeApiCall: updateUserInfo (uses withAuthRaw), deleteUser (uses withAuth), and
getNickname (uses withAuth) while getUserInfo already uses safeApiCall and
authPreference should be limited to logout; either migrate these methods to use
safeApiCall consistently (e.g., wrap
serverApi.updateUserInfo/deleteUser/getUserInfo calls inside safeApiCall and
stop passing authPreference into withAuth/withAuthRaw) or mark them explicitly
with TODO comments if you intend staged migration—update the code paths for
updateUserInfo, deleteUser, and getNickname to call safeApiCall with the same
error handling approach as getUserInfo and remove direct authPreference usage
(or add TODOs per method indicating planned migration).
feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt (1)

233-271: ⚡ Quick win

이전 구글 로그인 구현 주석 블록은 제거하는 편이 안전합니다.

대형 주석 코드가 남아 있으면 실제 동작 경로를 오해하기 쉽습니다. 현재 구현으로 확정됐다면 정리해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt`
around lines 233 - 271, Remove the large commented-out legacy Google login
implementation inside SocialAuthViewModel: delete the entire commented block
that contains the old loginWithGoogle(...) function and related logs
(_googleLoginState, googleLoginStatus, authRepository folding and
authPreference.saveTokens) so only the current active implementation remains;
ensure no leftover commented references to loginWithGoogle, googleLoginStatus,
_googleLoginState, authRepository, or authPreference are left behind.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/linku/MainViewModel.kt`:
- Around line 41-42: onLost currently unconditionally calls trySend(false),
causing false positives during network transitions; update the onLost(Network)
callback to re-check the system's active network state before emitting: obtain
the current active Network via ConnectivityManager.activeNetwork (or
connectivityManager.getActiveNetwork()) and inspect its NetworkCapabilities
(e.g., hasCapability(NET_CAPABILITY_INTERNET) and NET_CAPABILITY_VALIDATED) or
nullity, then call trySend(true/false) based on that result. Modify the onLost
handler in the registerNetworkCallback block (the onLost method that invokes
trySend) to perform this connectivityManager check and only emit false when no
validated/Internet-capable active network exists.

In `@app/src/main/java/com/linku/navigation/DoubleBackToExitIfTop.kt`:
- Around line 35-37: The BackHandler is enabled solely by checking currentRoute
in topLevelRoutes, causing it to intercept back presses even when the screen is
not the true top of the back stack; update the activation logic around
BackHandler(enabled = enabled) to also verify the nav back stack state (e.g.,
ensure there is no previous back stack entry) before enabling the handler so it
only runs when the destination is the actual top-level entry; reference
currentRoute, topLevelRoutes, enabled and BackHandler and use the NavController
backStack (previousBackStackEntry/currentBackStackEntry) or equivalent
backStackEntry check combined with the existing currentRoute check to gate the
handler.

In `@data/src/main/java/com/linku/data/api/TokenAuthenticator.kt`:
- Around line 53-63: The missing cleanup paths leave stale auth info; update the
TokenAuthenticator authenticate flow (the withLock block that reads refreshToken
and loginSessionStore.deviceId and the branches checking isSuccess/exception) to
call authPreference.clear() before every early return on failure (including when
refreshToken or deviceId is null, when the refresh request fails/isSuccess ==
false, and in exception handlers) so credentials are removed consistently;
ensure you reference and invoke authPreference.clear() in the same scopes where
you currently return@withLock null (and mirror this for the other similar blocks
around the ranges noted).

In `@data/src/main/java/com/linku/data/di/api/ServerApiModule.kt`:
- Around line 65-73: The network interceptor currently uses
addHeader("Authorization", ...) which appends headers and causes duplicate
Authorization on retries; change the interceptor in addNetworkInterceptor to
set/replace the header instead (use
chain.request().newBuilder().header("Authorization", "Bearer $token").build()
when authPreference.accessToken is not blank) so that when TokenAuthenticator
returns a new request the interceptor won't accumulate multiple Authorization
headers; ensure you still call chain.proceed(request).

In
`@data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt`:
- Around line 143-147: The code in AuthRepositoryImpl that reads
loginSessionStore.deviceId.first() currently throws ApiError.Auth.TokenExpired
when the deviceId is null; change this to throw a semantically appropriate error
(e.g., ApiError.Auth.DeviceNotFound or another ApiError.Auth subtype) instead of
TokenExpired. Locate the deviceId retrieval in the method that uses
loginSessionStore.deviceId.first(), add or use an existing DeviceNotFound error
class under ApiError.Auth (or create it), and replace the thrown
ApiError.Auth.TokenExpired(...) with ApiError.Auth.DeviceNotFound(...) including
the same code and message.

In `@feature/login/src/main/java/com/linku/login/navigation/LoginApp.kt`:
- Line 16: The import for hiltViewModel is wrong: replace the current import of
androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel with the correct package
androidx.hilt.navigation.compose.hiltViewModel used across the project and
declared in dependencies so the symbol hiltViewModel resolves; update the import
line in LoginApp.kt to reference androidx.hilt.navigation.compose.hiltViewModel.

In `@feature/login/src/main/java/com/linku/login/ui/item/StepIndicator.kt`:
- Around line 121-123: The checkmark Image used in StepIndicator is decorative
and being announced by screen readers; update the Image calls (in StepIndicator)
that currently pass a non-null content description (the instances around the
check icon) to instead use contentDescription = null so the icon is treated as a
decoration and not read aloud by accessibility services—apply this change to
both Image occurrences referenced in the diff.

In
`@feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt`:
- Around line 152-164: When handling the branch where result.refreshToken ==
null in SocialAuthViewModel, explicitly clear existing stored auth tokens before
setting authPreference.socialToken to avoid leftover
accessToken/refreshToken/userId being used; call the appropriate token-clear
method on authPreference (or saveTokens with nulls) prior to assigning
socialToken and emitting _kakaoLoginState.value =
SocialLoginState.Success(result). Apply the same change to the analogous
"refreshToken == null" branch for the other social-login block in this class so
both branches clear previous tokens before storing the temporary social token.
- Around line 220-231: loginWithGoogle currently sets _googleLoginState to
SocialLoginState.Loading but does not handle exceptions thrown before or during
authRepository.loginWithGoogle(...) so Loading can get stuck; wrap the body of
loginWithGoogle in a try/catch around the call to
authRepository.loginWithGoogle(...) (including the fold) and in the catch set
_googleLoginState to a GoogleLoginStatus.Error with a sensible message (use the
caught exception's message or a default like "구글 로그인 실패") and perform the same
.work(authPreference){ _googleLoginState.value = it } behavior so state is
always updated on failure; keep the existing fold logic inside the try block.

---

Outside diff comments:
In `@app/src/main/java/com/linku/MainApp.kt`:
- Around line 258-307: autoLoginTried is never set true so the retry-guard never
triggers; update state handling so autoLoginTried is toggled when an auto-login
attempt starts (or immediately in onResult before calling
loginViewModel.tryAutoLogin) and/or when the attempt completes (in onSuccess and
onFail) to prevent repeated retries; reference the variables/functions Splash,
onResult, autoLoginTried, and loginViewModel.tryAutoLogin and either set
autoLoginTried = true right before invoking tryAutoLogin or hoist this flag into
LoginViewModel (preferred) so the boolean is updated reliably across
recompositions and restored appropriately (if persisted use rememberSaveable,
otherwise remember/VM state).
- Around line 115-127: This LaunchedEffect(loginState) is duplicating navigation
already handled elsewhere (splash auto-login and onLoginSuccess); remove the
navigator.navigate(...) block from this LaunchedEffect and factor only the
shared post-login work into a helper (e.g., handlePostLogin or
loginViewModel.handlePostLogin) that performs homeViewModel.refreshAfterLogin()
and sets showNavBar = true, then call that helper from the single canonical
navigation sites (onLoginSuccess and the splash auto-login handler) so
navigation (navigator.navigate(NavigationRoute.Home.route) / popUpTo /
launchSingleTop) is executed only once.

In `@core/src/main/java/com/linku/core/repository/AuthRepository.kt`:
- Around line 50-58: The two methods loginWithKakao and loginWithGoogle return
inconsistent types (LoginResult vs Result<LoginResult>); make them consistent by
choosing one approach (preferably both return Result<LoginResult> for unified
error handling) and update the function signatures for loginWithKakao and
loginWithGoogle to match, then adjust their implementations, callers, and any
tests to handle Result<LoginResult> (or, if you choose exceptions, change both
to return LoginResult and propagate/handle exceptions consistently). Ensure the
service/repository interface and any implementing classes reference the same
return type and that imports for Result and LoginResult are correct.

In `@data/src/main/java/com/linku/data/api/ServerApiExt.kt`:
- Around line 135-160: The 401 recovery currently skips the actual token
reissue, so inside ServerApi.withTokenRefresh (the catch where
shouldRefreshToken(exception) is true) call the token refresh routine
(refreshTokenIfNeeded(authPreference)) before retrying block(); use the existing
refreshTokenIfNeeded(authPreference) helper (which handles refreshMutex and
updates AuthPreference.refreshToken/accessToken) and keep the retry block() and
the outer error handling/propagation intact so that failed refreshes still throw
the refreshError.

In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`:
- Around line 485-510: The method updateLinkFolder currently maps the API
response into result (using safeApiCall -> serverApi.updateLinkFolder and
constructing a LinkItemInfo) but then returns the original linku parameter
instead of the mapped result; fix this in updateLinkFolder by returning the
constructed LinkItemInfo (or assigning result to linku before return) so callers
receive the server-updated values (use the result from the safeApiCall mapping),
and ensure the mapping code that builds LinkItemInfo (fields linkuId,
parentFolderId, title, url/domain, linkuImageUrl, createdAt) is used as the
method return value.

---

Nitpick comments:
In `@data/src/main/java/com/linku/data/di/repository/CurationRepositoryModule.kt`:
- Around line 15-37: Remove the leftover commented-out implementation and inline
memo from CurationRepositoryModule: delete the commented provideLinkuRepository
block and the Korean temporary note ("/* 미래의 제가 수정할겁니다. */") so the module only
contains the active DI provider (provideCurationRepository) and related imports;
ensure no stray review comments remain and keep function names
(CurationRepositoryModule, provideCurationRepository) and added parameter
curationApi intact.

In `@data/src/main/java/com/linku/data/di/repository/LinkuRepositoryModule.kt`:
- Around line 19-21: 메서드명과 바인딩 대상이 불일치합니다: 현재 bindFolderRepository가
LinkuRepositoryImpl을 LinkuRepository에 바인딩하고 있어 혼동을 유발하므로 bindFolderRepository의
이름을 LinkuRepository로 바인딩하는 의도에 맞게 변경하세요; 구체적으로 LinkuRepositoryModule에서 함수명
bindFolderRepository를 LinkuRepository(또는 bindLinkuRepository)로 바꾸어
LinkuRepositoryImpl -> LinkuRepository 바인딩을 명확히 하세요.

In
`@data/src/main/java/com/linku/data/implementation/preference/AuthPreferenceImpl.kt`:
- Line 89: The comment on the refreshToken parameter in AuthPreferenceImpl.kt is
ambiguous; replace "//TODO : 수정하기." with a clear, actionable note: either
resolve the TODO now (describe the intended change, e.g., "make refreshToken
non-nullable once migration completes" or "store refresh token encrypted using X
method") or mark it as a tracked follow-up with an issue ID and expected
owner/ETA (e.g., "TODO: revisit nullability & encryption — see ISSUE-123
assigned to `@owner` by yyyy-mm-dd"). Update the comment adjacent to the
refreshToken parameter so reviewers know whether this PR addresses it or it is
deferred and what exactly must be changed.

In
`@data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt`:
- Around line 245-273: Remove the dead commented Google login block: delete the
entire commented-out loginWithGoogle function and its related commented
imports/usages (the block referencing loginWithGoogle, SocialLoginResponseDTO,
SocialLoginRequestDTO, and serverApi.withErrorHandling) so the file no longer
contains unused commented code; rely on VCS to retain history if needed.
- Around line 210-216: The Kakao login branch constructs a LoginResult with
inactiveDate set to an empty string, which is inconsistent with the email
sign-up branch that uses emailSignUpResponse.inactiveDate; update the Kakao flow
to read the actual inactiveDate from the social response (e.g.,
response.inactiveDate) if present, or set it to null when the API doesn't
provide it, instead of "" so LoginResult.inactiveDate usage is consistent across
flows; adjust any nullable types or LoginResult constructor if needed to accept
null.

In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`:
- Around line 461-465: 파일의 FolderRepositoryImpl 클래스에 남아 있는 주석 처리된 코드 블록
(safeApiCall204 { serverApi.deleteSharedFolder(folderId) })을 제거하세요; 해당 주석은
테스트용으로 보이므로 병합 전 삭제해야 하며, 관련된 메서드 이름 deleteSharedFolder 및 유틸 호출 safeApiCall204를
참고해 주석만 삭제하고 기능적 변경은 하지 마세요.

In
`@data/src/main/java/com/linku/data/implementation/repository/LinkuRepositoryImpl.kt`:
- Around line 137-144: The when-check on raw is likely redundant because
safeApiCall already unwraps BaseResponse<T>; inspect safeApiCall's return type
and, if it returns the inner T for serverApi.recentLinks, simplify by removing
the BaseResponse<*> branch and directly map/convert raw (the result of
safeApiCall { serverApi.recentLinks(limit = limit) }) to List<LinkuSimpleDTO>
(e.g., filterIsInstance or a safe cast) so the code uses a single, correct path
for obtaining list: List<LinkuSimpleDTO>.

In
`@data/src/main/java/com/linku/data/implementation/repository/UserRepositoryImpl.kt`:
- Around line 108-140: Several methods are inconsistent in migrating to
safeApiCall: updateUserInfo (uses withAuthRaw), deleteUser (uses withAuth), and
getNickname (uses withAuth) while getUserInfo already uses safeApiCall and
authPreference should be limited to logout; either migrate these methods to use
safeApiCall consistently (e.g., wrap
serverApi.updateUserInfo/deleteUser/getUserInfo calls inside safeApiCall and
stop passing authPreference into withAuth/withAuthRaw) or mark them explicitly
with TODO comments if you intend staged migration—update the code paths for
updateUserInfo, deleteUser, and getNickname to call safeApiCall with the same
error handling approach as getUserInfo and remove direct authPreference usage
(or add TODOs per method indicating planned migration).

In
`@feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt`:
- Around line 233-271: Remove the large commented-out legacy Google login
implementation inside SocialAuthViewModel: delete the entire commented block
that contains the old loginWithGoogle(...) function and related logs
(_googleLoginState, googleLoginStatus, authRepository folding and
authPreference.saveTokens) so only the current active implementation remains;
ensure no leftover commented references to loginWithGoogle, googleLoginStatus,
_googleLoginState, authRepository, or authPreference are left behind.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aec7641a-f116-4ec3-8f1c-b2ae4315f677

📥 Commits

Reviewing files that changed from the base of the PR and between 66b3c07 and 3e09183.

📒 Files selected for processing (76)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/linku/MainActivity.kt
  • app/src/main/java/com/linku/MainApp.kt
  • app/src/main/java/com/linku/MainApplication.kt
  • app/src/main/java/com/linku/MainViewModel.kt
  • app/src/main/java/com/linku/navigation/DoubleBackToExitIfTop.kt
  • core/src/main/java/com/linku/core/datastore/session/LoginSessionStore.kt
  • core/src/main/java/com/linku/core/error/ApiError.kt
  • core/src/main/java/com/linku/core/repository/AuthRepository.kt
  • data/src/main/java/com/linku/data/api/ApiExt.kt
  • data/src/main/java/com/linku/data/api/AuthApi.kt
  • data/src/main/java/com/linku/data/api/AuthQualifiers.kt
  • data/src/main/java/com/linku/data/api/ServerApi.kt
  • data/src/main/java/com/linku/data/api/ServerApiExt.kt
  • data/src/main/java/com/linku/data/api/TokenAuthenticator.kt
  • data/src/main/java/com/linku/data/api/dto/auth/refreshToken/ReissueRequestDTO.kt
  • data/src/main/java/com/linku/data/api/dto/auth/signup/email/CheckEmailCodeRequestDTO.kt
  • data/src/main/java/com/linku/data/api/dto/auth/signup/email/EmailCodeRequestDTO.kt
  • data/src/main/java/com/linku/data/api/dto/auth/signup/email/EmailVerifyRequestDTO.kt
  • data/src/main/java/com/linku/data/api/dto/auth/signup/email/SendEmailCodeRequestDTO.kt
  • data/src/main/java/com/linku/data/api/mapToApiError.kt
  • data/src/main/java/com/linku/data/di/api/ServerApiModule.kt
  • data/src/main/java/com/linku/data/di/preference/AuthPreferenceModule.kt
  • data/src/main/java/com/linku/data/di/qualifier/DataStoreQualifiers.kt
  • data/src/main/java/com/linku/data/di/repository/AIArticleRepositoryModule.kt
  • data/src/main/java/com/linku/data/di/repository/AlarmRepositoryModule.kt
  • data/src/main/java/com/linku/data/di/repository/AuthRepositoryModule.kt
  • data/src/main/java/com/linku/data/di/repository/CategoryRepositoryModule.kt
  • data/src/main/java/com/linku/data/di/repository/CurationRepositoryModule.kt
  • data/src/main/java/com/linku/data/di/repository/FolderRepositoryModule.kt
  • data/src/main/java/com/linku/data/di/repository/GsonModule.kt
  • data/src/main/java/com/linku/data/di/repository/LinkuRepositoryModule.kt
  • data/src/main/java/com/linku/data/di/repository/RecentSearchRepositoryModule.kt
  • data/src/main/java/com/linku/data/di/repository/RepositoryModule.kt
  • data/src/main/java/com/linku/data/implementation/preference/AuthPreferenceImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/AIArticleRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/CategoryRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/CurationRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/LinkuRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/UserRepositoryImpl.kt
  • data/src/main/java/com/linku/data/preference/AuthPreference.kt
  • feature/login/src/main/java/com/linku/login/navigation/LoginApp.kt
  • feature/login/src/main/java/com/linku/login/ui/bottom_sheet/NoAnimBottomSheet.kt
  • feature/login/src/main/java/com/linku/login/ui/bottom_sheet/TermsAgreementSheet.kt
  • feature/login/src/main/java/com/linku/login/ui/content/TermsAgreementContent.kt
  • feature/login/src/main/java/com/linku/login/ui/item/LoginTextField.kt
  • feature/login/src/main/java/com/linku/login/ui/item/PasswordLoginTextField.kt
  • feature/login/src/main/java/com/linku/login/ui/item/StepIndicator.kt
  • feature/login/src/main/java/com/linku/login/ui/item/WrongRuleItem.kt
  • feature/login/src/main/java/com/linku/login/ui/layout/SignUpSelectionLayout.kt
  • feature/login/src/main/java/com/linku/login/ui/layout/SignUpStepLayout.kt
  • feature/login/src/main/java/com/linku/login/ui/model/EmailVerificationEvents.kt
  • feature/login/src/main/java/com/linku/login/ui/model/EmailVerificationUiState.kt
  • feature/login/src/main/java/com/linku/login/ui/model/TermsAgreementEvent.kt
  • feature/login/src/main/java/com/linku/login/ui/model/TermsAgreementState.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/EmailLoginScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/EmailVerificationScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/InterestContentScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/InterestPurposeScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/SignUpGenderScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/SignUpJobScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/SignUpNicknameScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/SignUpPasswordScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/EmailInputScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/SocialGenderScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/SocialInterestScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/SocialJobScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/SocialNicknameScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/SocialPurposeScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/terms/MarketingTermsScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/terms/PrivacyTermsScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/terms/ServiceTermsScreen.kt
  • feature/login/src/main/java/com/linku/login/viewmodel/LoginViewModel.kt
  • feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt
💤 Files with no reviewable changes (16)
  • app/src/main/java/com/linku/MainApplication.kt
  • data/src/main/java/com/linku/data/api/ServerApi.kt
  • data/src/main/java/com/linku/data/di/repository/GsonModule.kt
  • feature/login/src/main/java/com/linku/login/ui/item/WrongRuleItem.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/SocialNicknameScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/SocialGenderScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/SocialInterestScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/SocialPurposeScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/InterestPurposeScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/EmailInputScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/SignUpNicknameScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/SignUpGenderScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/SignUpPasswordScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/InterestContentScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/email/SignUpJobScreen.kt
  • feature/login/src/main/java/com/linku/login/ui/screen/social/SocialJobScreen.kt

Comment thread app/src/main/java/com/linku/MainViewModel.kt Outdated
Comment thread app/src/main/java/com/linku/navigation/DoubleBackToExitIfTop.kt Outdated
Comment thread data/src/main/java/com/linku/data/api/TokenAuthenticator.kt
Comment thread data/src/main/java/com/linku/data/di/api/ServerApiModule.kt Outdated
Comment thread data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt Outdated
Comment thread feature/login/src/main/java/com/linku/login/navigation/LoginApp.kt
Comment thread feature/login/src/main/java/com/linku/login/ui/item/StepIndicator.kt Outdated
Comment thread feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt Outdated
- `DoubleBackToExitIfTop`에서 `enabled` 판별 조건을 수정하여, 현재 경로가 최상위 경로(topLevelRoutes)에 있더라도 이전 백스택 엔트리가 최상위 경로인 경우에는 뒤로 가기 핸들러가 활성화되지 않도록 변경
- 이를 통해 최상위 경로 간 이동 시 앱이 즉시 종료 대기 상태(Double Back)로 진입하지 않고 정상적으로 이전 화면으로 돌아갈 수 있도록 개선
- `onLost` 콜백 발생 시 단순히 `false`를 반환하는 대신, `isNetworkAvailable()`을 호출하여 실제 네트워크 연결 상태를 재확인한 후 결과를 전달하도록 수정 (다중 네트워크 환경에서의 정확한 상태 반영)
- `addNetworkInterceptor`를 `addInterceptor`로 변경하여 네트워크 레벨 이전의 요청 단계에서 헤더를 수정하도록 조정
- 요청 헤더에 이미 `Authorization` 필드가 존재하는 경우, 중복 삽입을 방지하기 위해 기존 요청을 그대로 진행하는 방어 로직 추가
- `addHeader` 대신 `header` 메소드를 사용하여 기존에 존재할 수 있는 `Authorization` 값을 덮어쓰도록 변경
- 가독성 향상을 위해 토큰 삽입 조건부 로직의 코드 블록 구조 정리
- `ApiError` 클래스 내 `Auth.DeviceNotFound` 에러 타입 추가
- `AuthRepositoryImpl`에서 토큰 재발급 시 `deviceId`가 없을 경우 발생하는 에러를 `TokenExpired`에서 `Unauthorized`로 변경하여 의미 명확화
- 기기 정보 누락에 대한 에러 메시지 및 코드(`AUTH_DEVICE_NOT_FOUND`) 수정 반영
- `StepIndicator`에서 완료 상태를 표시하는 `ic_login_check` 이미지의 `contentDescription`을 `"completed"`에서 `null`로 변경하여 불필요한 접근성 레이블 제거 (단순 장식 요소로 처리)
- 카카오 및 구글 로그인 성공 시, `refreshToken`이 없는 임시 유저(회원가입 필요 상태)일 경우 `authPreference.clear()`를 호출하여 기존 토큰 정보를 초기화하도록 수정
- `loginWithGoogle` 함수에 `try-catch` 블록을 추가하여 네트워크 오류 등 예기치 않은 예외 발생 시 에러 상태(`SocialLoginState.Error`)를 UI에 전달하도록 개선
- 코루틴 취소 예외(`CancellationException`)는 상위로 재전파(rethrow)하여 비정상적인 상태 업데이트 방지
- 구글 로그인 실패 시 로그 기록(`Log.e`) 로직 추가
- `ApiError` 클래스 내부에 신규 에러 도메인 정의:
    - `Terms`: 유효하지 않은 약관 타입 에러 (TERMS4001) 추가
    - `AiArticle`: AI 요약 관련 NotFound, Duplicate, InternalServer 에러 추가
    - `OpenAi`: 응답 파싱 및 형식 오류 관련 에러 추가
    - `Crawler`: 웹페이지 본문 추출 실패 및 금지 관련 에러 추가
    - `Gemini`: 요청 과다, 타임아웃, 파싱 오류 등 Gemini API 관련 상세 에러 추가
- `mapToApiError.kt`에 상기 신규 에러 코드들에 대한 매핑 로직을 추가하여 서버 응답 처리 보완 (TERMS4001, AIARTICLE, OPENAI, CRAWLER, GEMINI 계열 코드 반영)
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

🧹 Nitpick comments (1)
core/src/main/java/com/linku/core/error/ApiError.kt (1)

542-545: ⚡ Quick win

중첩 클래스명이 상위 클래스명과 중복됩니다.

Gemini.ApiError는 최상위 ApiError와 이름이 같아 코드를 읽을 때 혼란을 줄 수 있습니다. 더 명확한 이름(예: GeminiCallFailed, ExternalApiFailed)으로 변경하는 것을 권장합니다.

♻️ 제안: 더 명확한 이름으로 변경
-        /** GEMINI5021 - Gemini API 호출 중 오류 */
-        class ApiError(
+        /** GEMINI5021 - Gemini API 호출 중 오류 */
+        class CallFailed(
             override val code: String,
             override val message: String
         ) : Gemini()

또는

-        class ApiError(
+        class ExternalApiFailed(
             override val code: String,
             override val message: String
         ) : Gemini()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/main/java/com/linku/core/error/ApiError.kt` around lines 542 - 545,
The nested class Gemini.ApiError shadows the top-level ApiError and causes
confusion; rename the nested class to a clearer, distinct name (e.g.,
GeminiCallFailed or ExternalApiFailed) and update all references/usages
accordingly (the nested class declaration `class ApiError(...): Gemini()` and
any code that constructs or matches `Gemini.ApiError`) so the type no longer
collides with the top-level `ApiError`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/main/java/com/linku/core/error/ApiError.kt`:
- Around line 3-6: ApiError currently calls Exception() with no message so
Exception.message stays null; change ApiError to accept and forward a message to
Exception (e.g., declare sealed class ApiError(override val message: String) :
Exception(message) and keep abstract val code: String) so all subclasses pass
their message up via their constructors, or alternatively update intermediate
sealed/abstract subclasses to accept/forward message to super; ensure final
subclasses supply the human-readable message when constructing their ApiError
subtype.

---

Nitpick comments:
In `@core/src/main/java/com/linku/core/error/ApiError.kt`:
- Around line 542-545: The nested class Gemini.ApiError shadows the top-level
ApiError and causes confusion; rename the nested class to a clearer, distinct
name (e.g., GeminiCallFailed or ExternalApiFailed) and update all
references/usages accordingly (the nested class declaration `class
ApiError(...): Gemini()` and any code that constructs or matches
`Gemini.ApiError`) so the type no longer collides with the top-level `ApiError`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31eb292f-8290-439a-be79-b19ab3e03d40

📥 Commits

Reviewing files that changed from the base of the PR and between 3e09183 and 3004321.

📒 Files selected for processing (8)
  • app/src/main/java/com/linku/MainViewModel.kt
  • app/src/main/java/com/linku/navigation/DoubleBackToExitIfTop.kt
  • core/src/main/java/com/linku/core/error/ApiError.kt
  • data/src/main/java/com/linku/data/api/mapToApiError.kt
  • data/src/main/java/com/linku/data/di/api/ServerApiModule.kt
  • data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt
  • feature/login/src/main/java/com/linku/login/ui/item/StepIndicator.kt
  • feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (7)
  • app/src/main/java/com/linku/navigation/DoubleBackToExitIfTop.kt
  • data/src/main/java/com/linku/data/api/mapToApiError.kt
  • data/src/main/java/com/linku/data/di/api/ServerApiModule.kt
  • feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt
  • feature/login/src/main/java/com/linku/login/ui/item/StepIndicator.kt
  • app/src/main/java/com/linku/MainViewModel.kt
  • data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt

Comment thread core/src/main/java/com/linku/core/error/ApiError.kt Outdated
Comment thread core/src/main/java/com/linku/core/error/ApiError.kt
Comment thread core/src/main/java/com/linku/core/error/ApiError.kt
- **API 호출 로직 개선**:
  - `safeApiCall`, `safeApiCallUnit`, `safeApiCall204` 함수가 예외를 throw하는 대신 `Result<T>` 타입을 반환하도록 변경
  - 공통 에러 핸들링 로직을 `handleApiExceptions` 인라인 함수로 추출하여 중복 코드 제거
  - `safeApiCallUnit`에서 반환 타입을 `Result<Unit>`으로 통일하고, `safeApiCall204`는 `Response<Unit>` 기반으로 성공 여부를 판단하도록 수정

- **`ApiError` 모델 최적화**:
  - `ApiError` 및 하위 봉인 클래스(Sealed Class) 구조에서 `code`와 `message`를 생성자 파라미터로 받도록 변경하여 코드 간소화
  - 모든 에러 클래스가 `Exception(message)`을 상속받아 표준 예외 처리 방식에 부합하도록 수정
  - `Gemini.ApiError` 클래스명을 `Gemini.GeminiApiError`로 변경하여 부모 클래스와의 명칭 충돌 방지
- `AuthRepository` 인터페이스 및 `AuthRepositoryImpl` 구현체의 모든 메서드 반환 타입을 `Result<T>`로 변경하여 에러 핸들링 구조 개선
- 로그인(`login`, `loginWithKakao`, `loginWithGoogle`) 성공 시 `AuthPreference`를 통해 액세스/리프레시 토큰 및 사용자 ID를 저장하는 로직 추가
- `AuthPreferenceImpl`에서 `refreshToken` getter 로직 수정 (빈 문자열 대신 `null` 처리 및 `isNotBlank` 체크 추가)
- `signUpWithEmail` 호출 시 필수 리스트(`purposeList`, `interestList`)의 유효성 검사 로직을 `Result.failure` 반환으로 변경
- `reissue` 메서드 내 기기 정보 조회 실패 시 예외 처리 로직 보완
- 불필요한 주석 및 `TODO` 제거 (SharedPreferences 암호화 관련 결정 사항 반영 등)
- `UserRepository` 인터페이스 및 구현체 내 `getUserInfo`의 반환 타입을 `UserInfo`에서 `Result<UserInfo>`로 변경하여 에러 핸들링 강화
- `safeApiCall`의 결과를 `map` 함수를 통해 `UserInfo` 도메인 모델로 변환하도록 구조 개선
- 유저 정보 조회 성공 시 실행되는 `loginSessionStore.saveLogin` 로직을 `onSuccess` 블록으로 이동하고 `try-catch`를 통한 예외 처리 추가
- 디버깅을 위한 로그 출력 형식 정리 및 불필요한 주석 제거
… 및 예외 처리 강화

- `FolderRepositoryImpl`, `LinkuRepositoryImpl`, `AIArticleRepositoryImpl`, `CategoryRepositoryImpl` 내의 모든 `safeApiCall` 및 `safeApiCall204` 호출부에 `.getOrThrow()`를 추가하여 에러 핸들링 구조 개선
- `FolderRepositoryImpl.updateBookmark`에서 기존 속성 직접 접근 대신 `map` 연산자를 사용하도록 수정
- `FolderRepositoryImpl.getParentfolders`의 결과 매핑 로직을 `getOrThrow()` 이후로 이동하여 안정성 확보
- `FolderRepositoryImpl.updateLinkFolder` 내 반환값 관련 주석(TODO) 추가
- 불필요하게 분리되어 있던 API 호출 코드를 체이닝 형식으로 정리하여 가독성 향상
- **HomeViewModel**
    - `recentLinks` 상태 관리 방식을 `MutableState`에서 `StateFlow`(`_recentLinks`)로 변경하여 데이터 흐름의 일관성 및 안정성 확보
    - `getUserInfo` 호출 시 불필요한 `runCatching` 중복 사용을 제거하고 도메인 레이어의 `Result`를 직접 처리하도록 수정
    - `jobId` 할당 시 불필요한 `toLong()` 변환 로직 제거

- **상태 관리 및 예외 처리 고도화**
    - `SocialAuthViewModel`, `LoginViewModel`, `SignUpViewModel`, `FileViewModel` 등에서 Repository의 `Result` 반환값에 대해 `getOrThrow()`를 적용하여 명시적인 에외 처리 흐름 반영
    - `LoginViewModel` 내 로그인 성공 시 토큰을 저장하던 로직이 `authRepository` 내부 혹은 상위 흐름으로 위임됨에 따라 해당 호출부 정리
    - 사용자 정보 조회 및 회원가입 시 `Result` 패턴 대응을 위한 호출 로직 업데이트 및 주석 정리 (fold 패턴 검토 메모 포함)
- `verifyEmailCode` 함수 내 기존 `try-catch` 구조를 `authRepository`의 반환 타입 변경에 맞춰 `onSuccess`/`onFailure` 확장 함수를 사용하는 흐름으로 수정
- 인증 성공 시 타이머 정지 및 `VerifySuccess` 상태 반영, 실패 시 `VerifyError` 처리 로직을 가독성 있게 개선
- 예외 발생 시 로그 출력 및 네트워크 에러 상태(`NETWORK_ERROR`) 할당 로직 유지
- `MainApp`에서 자동 로그인 성공 시 `homeViewModel.refreshAfterLogin()`을 호출하여 홈 화면 데이터를 최신화하도록 수정
- `HomeApp`에서 `recentLinks` 상태 구독 방식을 `collectAsStateWithLifecycle`로 변경하여 생명주기 안정성 확보
- 코드 내 주석 위치 조정 및 불필요한 import 정리 (`getValue` 추가 등)
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt (1)

526-528: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

예외가 재발생되지 않아 오류가 무시됩니다.

다른 모든 메서드들은 예외를 잡은 후 throw e로 다시 발생시키지만, deleteLink는 예외를 로깅만 하고 무시합니다. 이로 인해 삭제가 실패해도 호출자에게 성공한 것처럼 보이게 됩니다.

🐛 예외 재발생 추가
         } catch (e: Exception) {
             Log.d("FolderRepositoryImpl", "deleteLink error: $e")
+            throw e
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`
around lines 526 - 528, In deleteLink inside FolderRepositoryImpl, the catch
block currently logs the exception but swallows it; change the catch to rethrow
the caught exception after logging (i.e., keep Log.d("FolderRepositoryImpl",
"deleteLink error: $e") then add throw e) so callers receive the failure like
other methods; ensure you reference the same caught variable name (e) and
preserve logging before rethrowing.
🧹 Nitpick comments (4)
feature/file/src/main/java/com/linku/file/FileViewModel.kt (1)

346-346: ⚡ Quick win

개인 메모성 주석은 제거해 주세요.

Line 346 주석은 코드 의도 설명이 아니라 개인 커뮤니케이션이라 코드베이스에 노이즈를 만듭니다.

✂️ 제안 변경
-                //지민아 너가 판단해서 fold 패턴이 맞다고 생각하면 그거로 변경하는 것도 좋을듯?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/file/src/main/java/com/linku/file/FileViewModel.kt` at line 346,
Remove the personal note comment on FileViewModel (the inline comment "//지민아 너가
판단해서 fold 패턴이 맞다고 생각하면 그거로 변경하는 것도 좋을듯?") — delete it from the FileViewModel.kt
source or replace it with a concise, objective comment explaining the intended
code behavior or a proper TODO with owner and reason (e.g., "// TODO: consider
using fold pattern for X — if adopted, update Y"), so the codebase contains only
actionable or explanatory comments.
data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt (2)

226-233: 💤 Low value

소셜 로그인에서 inactiveDate 처리가 이메일 로그인과 다릅니다.

이메일 로그인은 response.inactiveDate를 사용하지만, 카카오/구글 로그인은 빈 문자열("")로 하드코딩되어 있습니다. 소셜 로그인 응답에도 inactiveDate 필드가 있다면 해당 값을 사용하고, 없다면 null이나 일관된 기본값 처리를 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt`
around lines 226 - 233, The social-login mapping in AuthRepositoryImpl currently
hardcodes inactiveDate = "" when constructing LoginResult; change it to use
response.inactiveDate when present and fall back to a consistent default (e.g.,
null or the same default used by email login) so inactiveDate handling matches
email login; update the LoginResult construction in the social login branch to
reference response.inactiveDate (or wrap it with a null-coalescing/default)
instead of the empty string to keep behavior consistent.

265-293: 💤 Low value

주석 처리된 이전 구현을 제거해주세요.

새로운 loginWithGoogle 구현이 완료되었으므로, 이전 구현 코드는 제거하는 것이 좋습니다. 버전 관리 시스템에서 히스토리를 확인할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt`
around lines 265 - 293, Remove the entire commented-out legacy implementation of
loginWithGoogle (the block that references SocialLoginResponseDTO,
SocialLoginRequestDTO, serverApi.withErrorHandling and the Log.d/Log.e
statements); keep the new loginWithGoogle implementation only so dead/commented
code is not left in AuthRepositoryImpl and rely on VCS history for the old
implementation.
data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt (1)

459-461: 💤 Low value

주석 처리된 코드를 제거하거나 의도를 명확히 해주세요.

updateViewerPermission 내에서 deleteSharedFolder 호출이 주석 처리되어 있습니다. 불필요한 코드라면 제거하고, 향후 구현 예정이라면 TODO 주석으로 의도를 명시해주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`
around lines 459 - 461, In updateViewerPermission, remove the commented-out call
to deleteSharedFolder (and its safeApiCall204 wrapper) if it's no longer needed;
if it's planned for future work, replace the commented block with a clear TODO
comment indicating intended behavior and a ticket/PR reference (e.g., "TODO:
call serverApi.deleteSharedFolder(folderId) after X - ISSUE-123"); ensure the
change references the updateViewerPermission function and the deleteSharedFolder
/ safeApiCall204 symbols so readers know the intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`:
- Around line 506-507: In FolderRepositoryImpl's updateLinkFolder method you map
the API response to result but return the original input linku, which discards
server-updated fields; change the method to return result (the mapped API
response) instead of linku, or if returning linku was intentional, add a clear
comment explaining why; alternatively, declare and assign result outside the try
block so it can be returned after the API call—ensure you reference the result
variable and the updateLinkFolder method when making the change.

---

Outside diff comments:
In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`:
- Around line 526-528: In deleteLink inside FolderRepositoryImpl, the catch
block currently logs the exception but swallows it; change the catch to rethrow
the caught exception after logging (i.e., keep Log.d("FolderRepositoryImpl",
"deleteLink error: $e") then add throw e) so callers receive the failure like
other methods; ensure you reference the same caught variable name (e) and
preserve logging before rethrowing.

---

Nitpick comments:
In
`@data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt`:
- Around line 226-233: The social-login mapping in AuthRepositoryImpl currently
hardcodes inactiveDate = "" when constructing LoginResult; change it to use
response.inactiveDate when present and fall back to a consistent default (e.g.,
null or the same default used by email login) so inactiveDate handling matches
email login; update the LoginResult construction in the social login branch to
reference response.inactiveDate (or wrap it with a null-coalescing/default)
instead of the empty string to keep behavior consistent.
- Around line 265-293: Remove the entire commented-out legacy implementation of
loginWithGoogle (the block that references SocialLoginResponseDTO,
SocialLoginRequestDTO, serverApi.withErrorHandling and the Log.d/Log.e
statements); keep the new loginWithGoogle implementation only so dead/commented
code is not left in AuthRepositoryImpl and rely on VCS history for the old
implementation.

In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`:
- Around line 459-461: In updateViewerPermission, remove the commented-out call
to deleteSharedFolder (and its safeApiCall204 wrapper) if it's no longer needed;
if it's planned for future work, replace the commented block with a clear TODO
comment indicating intended behavior and a ticket/PR reference (e.g., "TODO:
call serverApi.deleteSharedFolder(folderId) after X - ISSUE-123"); ensure the
change references the updateViewerPermission function and the deleteSharedFolder
/ safeApiCall204 symbols so readers know the intent.

In `@feature/file/src/main/java/com/linku/file/FileViewModel.kt`:
- Line 346: Remove the personal note comment on FileViewModel (the inline
comment "//지민아 너가 판단해서 fold 패턴이 맞다고 생각하면 그거로 변경하는 것도 좋을듯?") — delete it from the
FileViewModel.kt source or replace it with a concise, objective comment
explaining the intended code behavior or a proper TODO with owner and reason
(e.g., "// TODO: consider using fold pattern for X — if adopted, update Y"), so
the codebase contains only actionable or explanatory comments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 34379c34-8982-484c-8c52-477d446ef198

📥 Commits

Reviewing files that changed from the base of the PR and between 3004321 and 68f60f4.

📒 Files selected for processing (19)
  • app/src/main/java/com/linku/MainApp.kt
  • core/src/main/java/com/linku/core/error/ApiError.kt
  • core/src/main/java/com/linku/core/repository/AuthRepository.kt
  • core/src/main/java/com/linku/core/repository/UserRepository.kt
  • data/src/main/java/com/linku/data/api/ApiExt.kt
  • data/src/main/java/com/linku/data/implementation/preference/AuthPreferenceImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/AIArticleRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/AuthRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/CategoryRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/LinkuRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/UserRepositoryImpl.kt
  • feature/file/src/main/java/com/linku/file/FileViewModel.kt
  • feature/home/src/main/java/com/linku/home/HomeApp.kt
  • feature/home/src/main/java/com/linku/home/HomeViewModel.kt
  • feature/login/src/main/java/com/linku/login/viewmodel/EmailAuthViewModel.kt
  • feature/login/src/main/java/com/linku/login/viewmodel/LoginViewModel.kt
  • feature/login/src/main/java/com/linku/login/viewmodel/SignUpViewModel.kt
  • feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (7)
  • data/src/main/java/com/linku/data/implementation/repository/AIArticleRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/CategoryRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/preference/AuthPreferenceImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/UserRepositoryImpl.kt
  • data/src/main/java/com/linku/data/implementation/repository/LinkuRepositoryImpl.kt
  • feature/login/src/main/java/com/linku/login/viewmodel/SocialAuthViewModel.kt
  • app/src/main/java/com/linku/MainApp.kt

Comment on lines 506 to +507
Log.d("FolderRepositoryImpl", "updateLinkFolder return: $linku")
return linku
return linku //TODO: 지민님께 왜 result를 안 던지고 이걸 던진 이유 물어보기!
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 | 🟠 Major | ⚡ Quick win

API 응답 대신 입력값을 반환하고 있습니다.

result에 API 응답을 매핑했지만 반환값은 원본 입력 linku입니다. 이로 인해 서버에서 업데이트된 값(예: createdAt, 새 linkuImageUrl 등)이 무시됩니다. 의도적인 경우 TODO 대신 명확한 주석을 남겨주세요.

🔧 API 응답 반환으로 수정
         Log.d("FolderRepositoryImpl", "updateLinkFolder return: $linku")
-        return linku //TODO: 지민님께 왜 result를 안 던지고 이걸 던진 이유 물어보기!
+        return result

또는 result를 try 블록 바깥에서 선언해야 합니다:

+        val result: LinkItemInfo
+
         try {
             Log.d("FolderRepositoryImpl", "updateLinkFolder try")
 
-            val result = safeApiCall {
+            result = safeApiCall {
                 serverApi.updateLinkFolder(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`
around lines 506 - 507, In FolderRepositoryImpl's updateLinkFolder method you
map the API response to result but return the original input linku, which
discards server-updated fields; change the method to return result (the mapped
API response) instead of linku, or if returning linku was intentional, add a
clear comment explaining why; alternatively, declare and assign result outside
the try block so it can be returned after the API call—ensure you reference the
result variable and the updateLinkFolder method when making the change.

Comment thread data/src/main/java/com/linku/data/api/ApiExt.kt
Copy link
Copy Markdown
Collaborator

@ugmin1030 ugmin1030 left a comment

Choose a reason for hiding this comment

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

ApiError.kt 수정 요청 코멘트

Comment thread core/src/main/java/com/linku/core/error/ApiError.kt
Comment thread core/src/main/java/com/linku/core/error/ApiError.kt
Comment thread core/src/main/java/com/linku/core/error/ApiError.kt
Comment thread core/src/main/java/com/linku/core/error/ApiError.kt
Comment thread core/src/main/java/com/linku/core/error/ApiError.kt
}

// 앱 실행 시 실행하여 이전 계정 기록 삭제
// FIXME : 지민님한테 여쭈어보기. 매번 앱 실행할 때마다 최근 기록을 지우는 것보다는 로그아웃 때 지우는건 어떤지
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

올바른 의견입니다

Copy link
Copy Markdown
Collaborator

@codebidoof codebidoof left a comment

Choose a reason for hiding this comment

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

정말 고생 많으셨습니다! 최정 Approve는 팀장님이 하시는 거루..ㅎㅎ

Comment thread data/src/main/java/com/linku/data/api/ApiExt.kt
Comment thread data/src/main/java/com/linku/data/api/ApiExt.kt
Comment on lines +40 to +51
suspend fun <T> safeApiCall(
block: suspend () -> BaseResponse<T>
): Result<T> = handleApiExceptions {
val response = block()
if (!response.isSuccess) {
return@handleApiExceptions Result.failure(mapToApiError(response.code, response.message))
}
val result = response.result ?: return@handleApiExceptions Result.failure(
ApiError.Common.InternalServer(code = "COMMON500", message = "결과값이 없습니다.")
)
Result.success(result)
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

safeApiCall에 대한 개선점을 다음과 같이 드릴 수도 있을 것 같습니다! safeApiCall 메서드가 성공 시에 api의 응답값을 그대로 Result로 감싸서 반환하는 모습을 확인하였는데 이 함수가 repository단에서 쓰이는 것을 고려한다면 매퍼 함수가 들어갈 transform 파라미터를 하나 추가해서 성공 시 core모듈의 도메인 모델로의 변환까지 책임지게 한다면 더 좋을 것 같습니다!

suspend fun <T, R> safeApiCall(
    block: suspend () -> BaseResponse<T>,
    transform: (T) -> R // 매퍼 함수를 넣을 파라미터
): Result<R> = handleApiExceptions {
    val response = block()
    if (!response.isSuccess) {
        return@handleApiExceptions Result.failure(mapToApiError(response.code, response.message))
    }
    val result = response.result ?: return@handleApiExceptions Result.failure(
        ApiError.Common.InternalServer(code = "COMMON500", message = "결과값이 없습니다.")
    )
    Result.success(transform(result))
}

Comment on lines +29 to +46
/**
* 네트워크 에러.
* 서버 응답 없이 클라이언트에서 직접 생성되므로 기본 코드와 메시지를 사용한다.
*/
sealed class Network(code: String, message: String) : ApiError(code, message) {
/** 인터넷 연결 없음 (UnknownHostException, IOException) */
class NoConnection : Network(
code = "NETWORK_NO_CONNECTION",
message = "네트워크 연결을 확인해주세요."
)

/** 연결 시간 초과 (SocketTimeoutException) */
class Timeout : Network(
code = "NETWORK_TIMEOUT",
message = "연결 시간이 초과되었습니다."
)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이 부분은 ApiError 밖으로 분리하는 게 더 적절해 보입니다.
코드베이스를 파악해본 결과 ApiError는 백엔드에서 내려주는 상태 코드 기반 에러를 표현하는 역할에 가까운데, 주석에도 적혀 있듯이 Network는 서버 응답 이전에 발생하는 클라이언트 레벨의 예외라 성격이 조금 다른 것 같습니다. 예를 들면 다음과 같은 식으로 구조화 해 보는 거 어떨까요?

sealed class ApiError(
    val code: String,
    message: String
) : Exception(message)

sealed class NetworkError(
    message: String
) : Exception(message) {

    data object NoConnection : NetworkError(
        "네트워크 연결을 확인해주세요."
    )

    data object Timeout : NetworkError(
        "연결 시간이 초과되었습니다."
    )
}

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.

맞는 조언입니다!
네트워크는 api와 부관하기에 따로 분리하는게 맞다고 생각합니다!
맞춰서 수정하겠습니다:) 감사합니다!

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.

exception은 오브젝트로 만들면 안되기에 class로 구현하는 방향으로 수정해보겠습니다..!

Comment on lines 49 to 60
// 이메일 인증 코드 전송
@POST("auth/email/code")
suspend fun sendVerificationEmail(
@Query("email") email: String
): BaseResponse<String> // BaseResponse 형태임. 별도 클래스 생성 없음.
@Body body: EmailCodeRequestDTO
): BaseResponse<Any> // result {}

// 이메일 인증 코드 검증
@GET("auth/email/verify")
@POST("auth/email/verify")
suspend fun checkVerificationEmail(
@Query("email") email: String,
@Query("code") code: String
): BaseResponse<EmailVerificationResponseDTO>
@Body body: EmailVerifyRequestDTO
): BaseResponse<Any> // result {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APi명세서를 확인해봤는데 이 두 api에서 성공 시 응답(result)이 오는 것으로 확인하였습니다. 해당 부분을 Any로 처리하신 이유가 궁금해요!

import retrofit2.HttpException


internal fun mapToApiError(code: String, message: String): ApiError = when (code) {
Copy link
Copy Markdown
Collaborator

@codebidoof codebidoof May 24, 2026

Choose a reason for hiding this comment

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

해당 함수의 시그니처가 제가 느끼기에는 비교적 복잡한 감이 있었어요! 사실 PM님이 원하는 요구사항에 따라 다르긴 할 것 같은데, 저는 "core모듈의 도메인 에러 ApiError가 서버에서 오는 code나 message를 다 관심사로 두어야 하는가?" 라는 관점에서 의문점이 들긴 했습니다. 각 feature의 ui단에서는 code나 message까지 다 보기보단, "어떤 에러인지" 만 보고 요구사항에 맞는 모달창을 띄운다던가 하는 식으로 처리하는 게 더 효율적일 것 같거든요.

제가 생각하는 좋은 리팩토링 방향은 다음과 같습니다. core의 ApiError를 다음고 같이 수정하여 data 레이어와의 결합도를 줄이고,,

sealed class ApiError : Exception() {

    // 공통 에러
    sealed class Common : ApiError() {
        data object BadRequest : Common()
        data object Unauthorized : Common()
        data object Forbidden : Common()
        data object TooManyRequests : Common()
        data object InternalServer : Common()
    }

    /* ...중략... */

    sealed class Folder : ApiError() {
        data object NotFound : Folder()
        data object ParentNotFound : Folder()
        data object CategoryNotFound : Folder()
        data object CreateForbidden : Folder()
        data object UpdateForbidden : Folder()
        data object DeleteForbidden : Folder()
        data object AccessForbidden : Folder()
        data object NameConflict : Folder()
        data object InvalidCursor : Folder()
        data object OwnerNotFound : Folder()
        data object PermissionNotFound : Folder()
        data object InvitationNotFound : Folder()
        data object InvitationExpired : Folder()
        data object InvitationLinkNotFound : Folder()
        data object PermissionNotAllowed : Folder()
        data object OwnerUpdateNotAllowed : Folder()
        data object InvitationCreatorCannotAccept : Folder()
        data object InvalidPermissionType : Folder()
        data object BookmarkNotFound : Folder()
    }

    data object Unknown : ApiError()
}

maptoApiError에서는 다음과 같이 매핑하는 것이죠

internal fun mapToApiError(code: String): ApiError = when(code) {
    "COMMON400" -> ApiError.Common.BadRequest
    "COMMON401" -> ApiError.Common.Unauthorized

    /* ...중략... */

    "FOLDER_PARENT404" -> ApiError.Folder.ParentNotFound

    else -> ApiError.Unknown
}

이렇게 하면 data layer에서는 서버 응답(code/message)을 해석하는 역할만 담당하고, core/domain 레이어는 “어떤 에러인가” 라는 의미 자체에만 집중할 수 있어서 책임 분리 측면에서도 더 깔끔해질 수 있을 것 같아요.

음..근데 확실히 이건 PM님의 의견이 중요할 것 같네요. 만약 PM님이 서버에서 오는 메세지를 ui단에서 직접 사용하길 원한다면 기존 방식이 더 나을 것 같가도 해요.

Copy link
Copy Markdown
Collaborator Author

@KateteDeveloper KateteDeveloper May 24, 2026

Choose a reason for hiding this comment

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

서버로부터 받은 응답 그대로 ui에 이용하려고 이렇게 짰는데 pm님 결정에 따르겠습니다~!

Copy link
Copy Markdown
Collaborator

@codebidoof codebidoof May 24, 2026

Choose a reason for hiding this comment

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

자려다가 생각 난 게 하나 더 있어서 적어 봅니다! 매핑에는 http응답의 code만 사용하는 방향성은 유지하되 message의 경우에는 아예 ApiError에서 앱단의 도메인 규칙으로 정하는 것은 어떤지 제안 드려 봐요😀😀

사실 제가 http응답의 message에 대한 종속성을 가지는 것에 회의적인 이유가, 저번 프로젝트 당시에도 에러 처리를 해야 하는 일이 있었는데 그 때 서버의 에러 메세지와 ui에 띄어야 하는 메세지가 어떤 건 같고~ 어떤 건 다르고~ 와 같은 일이 있었거든요. 그 당시 저도 서버의 에러 message를 그대로 썼다가 머리가 많이 아팠던 기억이 있어서...

각설하고 다음과 같은 방식을 한번 제안 드려 봅니다!

sealed class ApiError : Exception() {
    abstract val title: String // 이 프로퍼티는 제가 임의로 만들어 봤서용
    abstract override val message: String // 요건 필수

    sealed class Common : ApiError() {
        data object BadRequest : Common() {
            override val title = "잘못된 요청"
            override val message = "요청 내용을 확인하고 다시 시도해주세요."
        }
        // ... 
    }

    sealed class Folder : ApiError() {
        data object NotFound : Folder() {
            override val title = "폴더를 찾을 수 없어요"
            override val message = "폴더가 삭제되었거나 존재하지 않아요."
        }
        data object ParentNotFound : Folder() {
            override val title = "상위 폴더를 찾을 수 없어요"
            override val message = "상위 폴더가 삭제되었거나 존재하지 않아요."
        }
        data object CreateForbidden : Folder() {
            override val title = "폴더를 만들 수 없어요"
            override val message = "폴더 생성 권한이 없어요."
        }
        // ...
    }
}

이런 식으로 저희 프론트단에서 백엔드의 message 자체엔 크게 의존하지 않고 ui에 띄울 message를 변경 및 유지보수가 유연하게 해 보는 건 어떨까요? 😊😊

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.

4 participants