✨ feat: 검색·공유 초대 및 링크 폴더 이동 흐름 통합 - #205
Conversation
- **서버 호스트 참조 방식 변경**
- `app` 모듈에서 `SERVER_DOMAIN` 문자열을 가공하던 방식 대신 `localProperties`의 `SERVER_HOST`를 직접 참조하도록 수정
- 프로퍼티 누락 또는 빈 값일 경우 명시적인 `GradleException`을 발생시키도록 가드 로직 적용
- **`feature:file` 모듈 빌드 설정 확장**
- `localProperties` 파일 로드 및 `SERVER_HOST` 프로퍼티 추출 로직 추가
- `defaultConfig` 내에 `SERVER_HOST`를 `buildConfigField`로 등록하여 모듈 내에서 소스 코드로 접근 가능하도록 개선
- 빌드 스크립트 내에서 `Properties` 및 `apply` 사용을 위한 임포트 추가
- **로그 메시지 포맷 개선**
- `LinkuLog` 내 메서드 이름을 대괄호(`[]`)로 감싸 메시지 앞에 노출하도록 변경하여 가독성 향상
- **위치 정보(`locate`) 출력 기능 추가**
- 각 로그 레벨(D, E, I, V, W, WTF)에 특정 위치 정보를 `<locate>` 형태로 포함할 수 있는 신규 오버로드 함수 정의
- **Throwable 지원 확장**
- 위치 정보와 함께 예외 객체(`Throwable`)를 전달받아 상세 스택 트레이스를 로그로 남길 수 있는 기능 구현
- **KDoc 최신화**
- 로그 출력 방식 변경(메시지 끝 -> 메시지 앞 대괄호)에 따라 관련 함수들의 가이드 주석 일괄 수정
- 공개 및 인증 Retrofit의 HTTP 로깅 인터셉터 제거 - 인증자와 액세스 토큰 부착 흐름은 유지하고 무로깅 계약 문서화
- 공개 Retrofit의 OkHttp 클라이언트가 애플리케이션 인터셉터를 설치하지 않는지 검증
- 큐레이션 BODY 로깅 인터셉터 제거 - 인증 네트워크 인터셉터와 전용 타임아웃 계약 유지 및 문서화
- 큐레이션 클라이언트의 애플리케이션 인터셉터 미등록 검증 - 인증 인터셉터와 읽기·연결·쓰기 타임아웃 유지 검증
- HTTP 원문 로깅 제거로 불필요해진 민감 정보 정제기 삭제 - 정제기 전용 단위 테스트 함께 삭제
- 6개 모듈의 미사용 logging-interceptor 의존성 제거 - Version Catalog의 로깅 라이브러리 별칭과 버전 삭제
- 공유 폴더 생성 응답을 초대 토큰으로 명시 - 초대 토큰의 민감성과 Exception 전파 계약 문서화
- 초대 정보 조회와 수락 구현의 예외 계약을 Exception으로 정정
- 공유 폴더 앱링크 주석의 프로토콜을 실제 HTTPS 설정에 맞게 정정
- `MainApp.kt`에 Compose Navigation의 인자(Arguments) 타입을 정의하고 처리하기 위한 `NavType` 및 `navArgument` 임포트 구문 추가
- **`LinkuApi` 내 링크 폴더 변경 API 명세 수정**
- 엔드포인트 경로 변경: `linku/{linkuid}` -> `linku/{linkuId}/folder`
- 경로 파라미터명 변경: `linkuid` -> `linkuId`
- 응답 데이터 타입을 기존 `LinkuResultDTO`에서 신규 DTO인 `LinkuFolderChangeResultDTO`로 변경
- API 함수에 대한 상세 KDoc 주석 추가
- **`LinkuFolderChangeResultDTO` 신규 생성**
- 링크 이동 결과(linkuId, folderId, folderName, 생성/수정 시각)를 담는 전용 응답 DTO 정의
- Moshi `@JsonClass` 적용 및 각 필드 Nullable 처리로 안정성 확보
- **`FolderRepositoryImpl` 로직 개선**
- `updateLinkFolder` 함수에 상세 KDoc 주석 추가
- `onSuccess` 블록 내에서 불필요하게 `LinkItemInfo`를 수동으로 재생성하던 로직 제거 및 로그 출력 구조 개선
- FileScreen의 loading 상태를 collectAsState로 수집하도록 Compose Runtime import 추가
- app과 feature:file의 SERVER_HOST 누락 예외 메시지를 실제 속성명에 맞게 변경
- MainViewModel에서 참조하지 않는 android.util.Log import 제거
- HomeScreen에서 참조하지 않는 collectAsState import 제거
- SearchViewModelTest의 가짜 저장소가 getLinkDetailWithShared를 구현하도록 변경
Walkthrough딥링크 초대 수락과 검색 기능을 각각 토큰 기반·상태 복원·결과 타입 기반 흐름과 페이징·원격 검색 기록 기반 흐름으로 전환했습니다. 관련 API, 저장소, ViewModel, Compose UI, 오류 매핑, 테스트 및 빌드 설정을 갱신했습니다. Changes딥링크 및 공유 폴더 초대
페이징 검색 및 검색 기록
오류 및 네트워크 구성
빌드 및 로깅
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 (1)
data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt (1)
458-467: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win링크 삭제가 실제 DELETE 요청을 수행하지 않습니다.
상세 조회 성공 후 로그만 남기고 삭제 API가 주석 처리되어 있어, 호출자는 성공으로 처리하지만 서버의 링크는 남습니다. 조회 결과의
userLinkuId가 필요하다면 다시 추출한 뒤serverApi.deleteLink(...)를 호출하고 실패를 전파하세요.🤖 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 458 - 467, Update the link-deletion flow around safeApiCall and serverApi.getDetailLink so that, after retrieving the detail result, it extracts the required userLinkuId and invokes serverApi.deleteLink(userLinkuId). Remove the commented-out deletion path, retain failure propagation, and ensure the operation is not reported successful until the DELETE request completes.
🧹 Nitpick comments (7)
data/src/main/java/com/linku/data/di/api/CurationApiModule.kt (1)
46-55: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift인터셉터에서 저장소 조회를 동기 블로킹하지 마세요.
AuthPreference.getAccessToken()은 DataStorefirst()호출까지 포함하는suspend함수인데, OkHttp 네트워크 인터셉터는 동기 계약이라runBlocking토큰 조회 동안 네트워크 요청을 처리하는 스레드가 점유됩니다. 토큰 상태를 데이터 흐름에 맞게 동기적으로 읽히도록 분리하세요.🤖 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/api/CurationApiModule.kt` around lines 46 - 55, Remove the runBlocking-based AuthPreference.getAccessToken() call from the CurationApiModule network interceptor and provide the token through a non-suspending, synchronously readable state prepared by the authentication/data flow. Update the interceptor to read that state without blocking while preserving the existing Authorization header behavior for non-blank tokens.app/src/main/java/com/linku/MainApp.kt (1)
918-937: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win예외를 삼키기 전에 로깅을 남겨 주세요.
세 개의 catch 블록이 토스트만 띄우고 원인 예외를 버립니다. 딥링크 실패는 재현이 어려운 편이라
LinkuLog(프로젝트 공통 로거)로 원인을 남기는 편이 디버깅에 유리합니다.🤖 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 918 - 937, Update the three exception handlers in the deep-link flow—IllegalArgumentException, Exception, and DeepLinkError.MissingInvitationToken—to log each caught exception with the shared LinkuLog before showing the existing toast; keep CancellationException propagation unchanged.Source: Linters/SAST tools
app/src/main/java/com/linku/deeplink/InvitationLinkRoute.kt (1)
32-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value기본값 없는 파라미터를 앞쪽으로 옮기는 것을 고려해 주세요.
navigator,deepLinkEntryId가 기본값 파라미터들 뒤에 있어 위치 인자 호출이 사실상 불가능합니다. 현재 호출부는 named argument라 문제는 없지만 시그니처 가독성 측면에서 필수 파라미터를 앞으로 모으는 편이 낫습니다.🤖 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/deeplink/InvitationLinkRoute.kt` around lines 32 - 45, Reorder the parameters in invitationLinkRoute so the required navigator and deepLinkEntryId parameters appear before the optional callback parameters with default values, preserving all parameter names and behavior.feature/home/src/main/java/com/linku/home/ui/home/bar/component/HomeSearchBar.kt (1)
27-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
feature:home컴포넌트가feature:file의R/테마를 참조합니다.
com.linku.file.R,com.linku.file.ui.theme.White의존은 모듈 경계를 넘는 참조라feature:file리소스 변경에 홈 화면이 깨질 수 있습니다. 공용 리소스는design모듈로 옮기는 것을 권장합니다.🤖 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/home/src/main/java/com/linku/home/ui/home/bar/component/HomeSearchBar.kt` around lines 27 - 28, HomeSearchBar의 com.linku.file.R 및 com.linku.file.ui.theme.White 참조를 제거하고, 해당 공용 리소스를 design 모듈로 이동한 뒤 design 모듈의 R과 테마 심볼을 사용하도록 import와 참조를 변경하세요.app/src/main/java/com/linku/search/SearchViewModel.kt (1)
79-191: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win세 메서드의 로딩/에러/requestId 처리 로직이 동일하게 반복됩니다.
loadRecentQueries/removeRecentQuery/clearRecentQueries가 같은 골격을 세 번 복제하고 있습니다.private fun runHistoryAction(block: suspend () -> Result<T>, onSuccess: (T) -> Unit)같은 헬퍼로 묶으면 유지보수가 쉬워집니다.🤖 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/search/SearchViewModel.kt` around lines 79 - 191, Extract the duplicated loading, cancellation, requestId validation, error handling, and final loading-state updates from loadRecentQueries, removeRecentQuery, and clearRecentQueries into a private generic runHistoryAction helper accepting the suspend Result-producing block and success callback. Update all three public methods to delegate through this helper while preserving their existing success-state updates and request cancellation behavior.design/src/main/java/com/linku/design/top/search/SearchBarTopSheet.kt (1)
159-165: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
LaunchedEffect(Unit)이 최초onQueryChange람다를 고정 캡처합니다.키가
Unit이라 이후 리컴포지션에서 콜백이 바뀌어도 반영되지 않습니다. 현재 호출부는 안정적인 메서드 참조를 넘기지만,rememberUpdatedState로 감싸두면 향후 람다 형태로 바뀌어도 안전합니다.♻️ 제안
+ val currentOnQueryChange by rememberUpdatedState(onQueryChange) LaunchedEffect(Unit) { snapshotFlow { text } .map { it.trim() } .debounce(800.milliseconds) .distinctUntilChanged() - .collectLatest(onQueryChange) + .collectLatest { currentOnQueryChange(it) } }🤖 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 `@design/src/main/java/com/linku/design/top/search/SearchBarTopSheet.kt` around lines 159 - 165, Update the LaunchedEffect(Unit) flow around onQueryChange to wrap the callback with rememberUpdatedState and collect the current callback value, so recompositions are reflected without restarting the effect. Preserve the existing debounce and distinctUntilChanged behavior.data/src/main/java/com/linku/data/mapper/SearchHistoryMapper.kt (1)
6-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
toDomain()가시성을internal로 통일 권장.같은
data.mapper패키지의LinkuSearchMapper.kt의toDomain()은internal로 선언된 반면, 이 함수는 기본(public) 가시성입니다. data 계층의 DTO→도메인 매핑 함수가 모듈 밖으로 노출되면 계층 분리 원칙이 흐려질 수 있으므로 일관되게internal로 제한하는 것이 좋습니다.♻️ 제안 수정
-fun SearchHistoryItemResponseDTO.toDomain(): RecentQuery = +internal fun SearchHistoryItemResponseDTO.toDomain(): RecentQuery = RecentQuery( searchHistoryId = searchHistoryId, keyword = keyword )As per coding guidelines,
**/{data,domain,ui}/**/*.kt: "Data source calls (API/DB) must be implemented indata/repository implementations, and UI code must access them only throughdomain/use cases."🤖 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/mapper/SearchHistoryMapper.kt` around lines 6 - 10, Update SearchHistoryItemResponseDTO.toDomain() to use internal visibility, matching the existing visibility of LinkuSearchMapper.toDomain() and preventing this data-layer mapper from being exposed outside the module.Source: Coding guidelines
🤖 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/build.gradle.kts`:
- Around line 37-40: Update the SERVER_HOST handling near the serverHost
property so the manifest receives only the hostname, excluding any scheme or
path such as https:// and /open. If full URLs are required elsewhere, parse the
value into separate components and align each manifest placeholder with the
corresponding component.
In `@app/src/main/java/com/linku/MainApp.kt`:
- Around line 417-474: Update the pending-invitation handling around
deepLinkViewModel.consumePendingInvitation() and the loginScope.launch block so
cancellation before the invitation result is finalized restores the consumed
token via deepLinkViewModel.setPendingInvitation(pendingInvitationToken). Ensure
restoration occurs for coroutine cancellation, while preserving the existing
AuthenticationRequired restoration and all result-specific handling.
In `@app/src/main/java/com/linku/NavigationDestination.kt`:
- Line 21: NavigationDestination.Login의 route 값이 auth_graph의 실제 목적지와 불일치합니다.
MainApp의 NavigationRoute.Login 사용 흐름과 LoginApp의 중첩 그래프를 확인해, 진입 목적지는 "login"으로
맞추고 feature/login 내부의 "login" 라우트와 login_root가 혼동되지 않도록 별도 라우트 심볼을 유지하거나 참조를
올바르게 분리하세요.
In `@app/src/main/java/com/linku/search/SearchViewModel.kt`:
- Around line 121-158: Update removeRecentQuery so it does not cancel or
overwrite the shared historyJob used by loadRecentQueries, allowing consecutive
removals to run independently. Preserve the existing requestId guards for stale
UI updates, while ensuring each successful removal filters its own
searchHistoryId from recentQueries and each request manages loading/error state
without canceling another deletion.
In `@data/src/test/java/com/linku/data/di/api/CurationApiModuleTest.kt`:
- Around line 23-36: The client tests only check interceptor counts, so they do
not verify logger removal or authentication registration. In
data/src/test/java/com/linku/data/di/api/CurationApiModuleTest.kt lines 23-36,
update curation client assertions to inspect networkInterceptors, confirm the
application logger is absent, and verify the authentication interceptor is
present while preserving timeout checks. In
data/src/test/java/com/linku/data/di/api/ServerApiModuleTest.kt lines 20-26,
also assert that the public client’s networkInterceptors contain no logger.
In `@design/src/main/java/com/linku/design/top/search/SearchBarTopSheet.kt`:
- Around line 625-634: Update the error display around uiState.errorMessage to
use a localized fixed string via stringResource instead of rendering the
exception message directly. In SearchViewModel, retain the original exception
details only in logging and stop storing them as the user-facing error message;
preserve the existing error-state flow.
- Around line 676-702: Update the empty-result branch in the SearchBarTopSheet
composable to check the current search query length before rendering the “search
results empty” Row. For queries shorter than two characters, hide that message
or show the existing minimum-length guidance instead; preserve the current
empty-results message for queries meeting the minimum length and prevent stale
results during the debounce interval from displaying it.
In `@feature/file/build.gradle.kts`:
- Around line 15-18: Update the localProperties initialization block to wrap the
file.inputStream() passed to Properties.load() in a use block, ensuring the
stream is closed after loading local.properties while preserving the existing
file-existence check and property loading behavior.
In `@feature/file/src/main/java/com/linku/file/FileViewModel.kt`:
- Line 3: Remove the direct android.net.Uri dependency from the ViewModel
methods buildInvitationLink and extractInvitationToken. Move their link parsing
and normalization logic into a pure Kotlin helper in the data or domain layer,
then have the ViewModel delegate to that helper while preserving existing
behavior and enabling JVM unit testing.
---
Outside diff comments:
In
`@data/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.kt`:
- Around line 458-467: Update the link-deletion flow around safeApiCall and
serverApi.getDetailLink so that, after retrieving the detail result, it extracts
the required userLinkuId and invokes serverApi.deleteLink(userLinkuId). Remove
the commented-out deletion path, retain failure propagation, and ensure the
operation is not reported successful until the DELETE request completes.
---
Nitpick comments:
In `@app/src/main/java/com/linku/deeplink/InvitationLinkRoute.kt`:
- Around line 32-45: Reorder the parameters in invitationLinkRoute so the
required navigator and deepLinkEntryId parameters appear before the optional
callback parameters with default values, preserving all parameter names and
behavior.
In `@app/src/main/java/com/linku/MainApp.kt`:
- Around line 918-937: Update the three exception handlers in the deep-link
flow—IllegalArgumentException, Exception, and
DeepLinkError.MissingInvitationToken—to log each caught exception with the
shared LinkuLog before showing the existing toast; keep CancellationException
propagation unchanged.
In `@app/src/main/java/com/linku/search/SearchViewModel.kt`:
- Around line 79-191: Extract the duplicated loading, cancellation, requestId
validation, error handling, and final loading-state updates from
loadRecentQueries, removeRecentQuery, and clearRecentQueries into a private
generic runHistoryAction helper accepting the suspend Result-producing block and
success callback. Update all three public methods to delegate through this
helper while preserving their existing success-state updates and request
cancellation behavior.
In `@data/src/main/java/com/linku/data/di/api/CurationApiModule.kt`:
- Around line 46-55: Remove the runBlocking-based
AuthPreference.getAccessToken() call from the CurationApiModule network
interceptor and provide the token through a non-suspending, synchronously
readable state prepared by the authentication/data flow. Update the interceptor
to read that state without blocking while preserving the existing Authorization
header behavior for non-blank tokens.
In `@data/src/main/java/com/linku/data/mapper/SearchHistoryMapper.kt`:
- Around line 6-10: Update SearchHistoryItemResponseDTO.toDomain() to use
internal visibility, matching the existing visibility of
LinkuSearchMapper.toDomain() and preventing this data-layer mapper from being
exposed outside the module.
In `@design/src/main/java/com/linku/design/top/search/SearchBarTopSheet.kt`:
- Around line 159-165: Update the LaunchedEffect(Unit) flow around onQueryChange
to wrap the callback with rememberUpdatedState and collect the current callback
value, so recompositions are reflected without restarting the effect. Preserve
the existing debounce and distinctUntilChanged behavior.
In
`@feature/home/src/main/java/com/linku/home/ui/home/bar/component/HomeSearchBar.kt`:
- Around line 27-28: HomeSearchBar의 com.linku.file.R 및
com.linku.file.ui.theme.White 참조를 제거하고, 해당 공용 리소스를 design 모듈로 이동한 뒤 design 모듈의
R과 테마 심볼을 사용하도록 import와 참조를 변경하세요.
🪄 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 Plus
Run ID: 6a908632-7e1f-4064-ba28-8d431b9cf51d
📒 Files selected for processing (78)
app/build.gradle.ktsapp/src/main/AndroidManifest.xmlapp/src/main/java/com/linku/MainActivity.ktapp/src/main/java/com/linku/MainApp.ktapp/src/main/java/com/linku/MainViewModel.ktapp/src/main/java/com/linku/NavigationDestination.ktapp/src/main/java/com/linku/deeplink/DeepLinkHandlerViewModel.ktapp/src/main/java/com/linku/deeplink/HandleNewIntentDeepLinks.ktapp/src/main/java/com/linku/deeplink/InvitationLinkRoute.ktapp/src/main/java/com/linku/deeplink/OpenDeepLinkParser.ktapp/src/main/java/com/linku/search/SearchViewModel.ktapp/src/main/res/values/strings.xmlapp/src/test/java/com/linku/deeplink/DeepLinkHandlerViewModelTest.ktapp/src/test/java/com/linku/deeplink/OpenDeepLinkParserTest.ktapp/src/test/java/com/linku/search/SearchViewModelTest.ktcore/build.gradle.ktscore/src/main/java/com/linku/core/error/ApiError.ktcore/src/main/java/com/linku/core/error/DeepLinkError.ktcore/src/main/java/com/linku/core/model/deeplink/DeepLinkType.ktcore/src/main/java/com/linku/core/model/search/FastSearchLinkInfo.ktcore/src/main/java/com/linku/core/model/search/LinkuSearchInfo.ktcore/src/main/java/com/linku/core/model/search/RecentQuery.ktcore/src/main/java/com/linku/core/repository/LinkuRepository.ktcore/src/main/java/com/linku/core/repository/RecentSearchRepository.ktcore/src/main/java/com/linku/core/usecase/AcceptSharedFolderInvitationUseCase.ktcore/src/main/java/com/linku/core/util/logging/LinkuLog.ktcore/src/main/java/com/linku/core/util/logging/LinkuLogD.ktcore/src/main/java/com/linku/core/util/logging/LinkuLogE.ktcore/src/main/java/com/linku/core/util/logging/LinkuLogI.ktcore/src/main/java/com/linku/core/util/logging/LinkuLogV.ktcore/src/main/java/com/linku/core/util/logging/LinkuLogW.ktcore/src/main/java/com/linku/core/util/logging/LinkuLogWTF.ktcore/src/test/java/com/linku/core/usecase/AcceptSharedFolderInvitationUseCaseTest.ktdata/build.gradle.ktsdata/src/main/java/com/linku/data/api/LinkuApi.ktdata/src/main/java/com/linku/data/api/SearchHistoryApi.ktdata/src/main/java/com/linku/data/api/dto/folder/LinkuFolderChangeResultDTO.ktdata/src/main/java/com/linku/data/api/dto/search/LinkuSearchResponseDTO.ktdata/src/main/java/com/linku/data/api/dto/search/SearchHistoryItemResponseDTO.ktdata/src/main/java/com/linku/data/api/dto/server/ApiResponseQuickSearchResult.ktdata/src/main/java/com/linku/data/api/dto/server/QuickSearchResult.ktdata/src/main/java/com/linku/data/api/mapToApiError.ktdata/src/main/java/com/linku/data/di/api/CurationApiModule.ktdata/src/main/java/com/linku/data/di/api/SearchHistoryApiModule.ktdata/src/main/java/com/linku/data/di/api/ServerApiModule.ktdata/src/main/java/com/linku/data/di/qualifier/DataStoreQualifiers.ktdata/src/main/java/com/linku/data/di/repository/RecentSearchRepositoryModule.ktdata/src/main/java/com/linku/data/implementation/repository/FolderRepositoryImpl.ktdata/src/main/java/com/linku/data/implementation/repository/InvitationRepositoryImpl.ktdata/src/main/java/com/linku/data/implementation/repository/LinkuRepositoryImpl.ktdata/src/main/java/com/linku/data/implementation/repository/LinkuSearchPagingSource.ktdata/src/main/java/com/linku/data/implementation/repository/RecentSearchRepositoryImpl.ktdata/src/main/java/com/linku/data/mapper/LinkuSearchMapper.ktdata/src/main/java/com/linku/data/mapper/SearchHistoryMapper.ktdata/src/test/java/com/linku/data/di/api/CurationApiModuleTest.ktdata/src/test/java/com/linku/data/di/api/ServerApiModuleTest.ktdata/src/test/java/com/linku/data/implementation/repository/LinkuSearchPagingSourceTest.ktdata/src/test/java/com/linku/data/implementation/repository/RecentSearchRepositoryImplTest.ktdata/src/test/java/com/linku/data/mapper/LinkuSearchMapperTest.ktdata/src/test/java/com/linku/data/mapper/SearchHistoryMapperTest.ktdesign/build.gradle.ktsdesign/src/main/java/com/linku/design/top/search/SearchBarTopSheet.ktdesign/src/main/res/values/strings.xmlfeature/file/build.gradle.ktsfeature/file/src/main/java/com/linku/file/FileApp.ktfeature/file/src/main/java/com/linku/file/FileScreen.ktfeature/file/src/main/java/com/linku/file/FileViewModel.ktfeature/file/src/main/java/com/linku/file/ui/top/bar/FileTopBar.ktfeature/file/src/main/java/com/linku/file/viewmodel/folder/state/FolderStateViewModel.ktfeature/home/build.gradle.ktsfeature/home/src/main/java/com/linku/home/HomeApp.ktfeature/home/src/main/java/com/linku/home/HomeViewModel.ktfeature/home/src/main/java/com/linku/home/screen/HomeScreen.ktfeature/home/src/main/java/com/linku/home/ui/home/bar/HomeTopBar.ktfeature/home/src/main/java/com/linku/home/ui/home/bar/component/HomeSearchBar.ktfeature/login/build.gradle.ktsgradle/libs.versions.tomltest/login/build.gradle.kts
💤 Files with no reviewable changes (7)
- core/src/main/java/com/linku/core/model/search/FastSearchLinkInfo.kt
- data/src/main/java/com/linku/data/di/qualifier/DataStoreQualifiers.kt
- data/src/main/java/com/linku/data/api/dto/server/ApiResponseQuickSearchResult.kt
- data/src/main/java/com/linku/data/api/dto/server/QuickSearchResult.kt
- core/src/main/java/com/linku/core/model/deeplink/DeepLinkType.kt
- app/src/main/java/com/linku/MainViewModel.kt
- gradle/libs.versions.toml
| val serverHost = localProperties.getProperty("SERVER_HOST") | ||
| ?.trim() | ||
| ?.takeIf { it.isNotEmpty() } | ||
| ?: throw GradleException("SERVER_HOST is missing or blank in local.properties") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -a '^AndroidManifest\.xml$' app | while read -r manifest; do
echo "===== $manifest ====="
rg -n -C 4 'SERVER_HOST|SERVER_SCHEME|SERVER_PATH|intent-filter|android:host|android:scheme|android:path' "$manifest" || true
done
rg -n -C 2 'SERVER_HOST|SERVER_SCHEME|SERVER_PATH' app/build.gradle.kts README.md 2>/dev/null || trueRepository: LinkYou-2025/LinkU_Android
Length of output: 3347
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "===== app/build.gradle.kts relevant section ====="
sed -n '1,90p' app/build.gradle.kts | cat -n
echo "===== manifest relevant section ====="
sed -n '45,60p' app/src/main/AndroidManifest.xml | cat -n
echo "===== README / requirements references ====="
fd -a 'README.*|PULL_REQUEST_TEMPLATE.*|.*\.md$' . | while read -r f; do
echo "--- $f ---"
rg -n -i -C 3 'app link|App Link|AppLink|Server Host|SERVER_HOST|scheme|host|path|open|https' "$f" || true
done
echo "===== local.properties template if present ====="
if [ -f app/local.properties ]; then
cat -n app/local.properties
elif [ -f .gitattributes ]; then
echo "no app/local.properties"
fiRepository: LinkYou-2025/LinkU_Android
Length of output: 6621
App Link의 SERVER_HOST를 호스트만 전달하세요.
현재 manifest는 android:scheme="https"와 android:path="/open"을 별도로 설정하면서 android:host="${SERVER_HOST}"로 연결하고 있습니다. 만약 SERVER_HOST에 https://example.com/open 같은 scheme/path까지 포함되면 manifest가 잘못 계산되어 App Link 매칭이 무너집니다. SERVER_HOST는 호스트 정보만 전달하고, PR 목표에서 URL 전체값이 필요하다면 구문 분리 뒤 separate placeholder에 맞게 매니페스트도 맞춰 조정하세요.
🤖 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/build.gradle.kts` around lines 37 - 40, Update the SERVER_HOST handling
near the serverHost property so the manifest receives only the hostname,
excluding any scheme or path such as https:// and /open. If full URLs are
required elsewhere, parse the value into separate components and align each
manifest placeholder with the corresponding component.
| val pendingInvitationToken = | ||
| deepLinkViewModel.consumePendingInvitation() | ||
|
|
||
| if (pendingInvitationToken.isNotBlank()) { | ||
| loginScope.launch { | ||
| when ( | ||
| fileViewModel.receiveSharedFolderInvitation( | ||
| pendingInvitationToken | ||
| ) | ||
| ) { | ||
| is AcceptSharedFolderInvitationResult.Accepted -> { | ||
| openSharedFoldersFromLogin() | ||
| } | ||
|
|
||
| is AcceptSharedFolderInvitationResult.AcceptedButRefreshFailed -> { | ||
| // 초대 수락은 완료되었으므로 갱신 실패를 알리고 공유 폴더를 엽니다. | ||
| Toast.makeText( | ||
| context, | ||
| R.string.share_folder_refresh_failed, | ||
| Toast.LENGTH_SHORT, | ||
| ).show() | ||
| openSharedFoldersFromLogin() | ||
| } | ||
|
|
||
| // TODO: 지민님 딥링크 대기 작업 처리 확인 필요 요청하기. | ||
| is AcceptSharedFolderInvitationResult.AuthenticationRequired -> { | ||
| // 소비한 토큰을 복원해 다음 로그인 성공 후 초대 수락을 재시도합니다. | ||
| deepLinkViewModel.setPendingInvitation( | ||
| pendingInvitationToken | ||
| ) | ||
| Toast.makeText( | ||
| context, | ||
| R.string.authentication_required, | ||
| Toast.LENGTH_SHORT, | ||
| ).show() | ||
| navigateFromLoginTo(NavigationRoute.Login.route) | ||
| } | ||
|
|
||
| // 딥링크 대기 작업 처리 //지민아 이거 정리해줄 수 있어? | ||
| deepLinkViewModel.consumePendingInvitation().let { token -> | ||
| fileViewModel.receiveSharedFolderInvitation(token) | ||
| folderStateViewModel.updateIsSharedFolders(true) | ||
| is AcceptSharedFolderInvitationResult.InvalidInvitation -> { | ||
| handlePendingInvitationFailure( | ||
| R.string.invalid_share_link | ||
| ) | ||
| } | ||
|
|
||
| navigator.navigate(NavigationRoute.File.route) { | ||
| popUpTo("login_root") { inclusive = true } | ||
| launchSingleTop = true | ||
| is AcceptSharedFolderInvitationResult.NetworkFailure -> { | ||
| handlePendingInvitationFailure( | ||
| R.string.network_error | ||
| ) | ||
| } | ||
|
|
||
| is AcceptSharedFolderInvitationResult.Failure -> { | ||
| handlePendingInvitationFailure( | ||
| R.string.undefined_behavior | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| return@LoginApp | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
소비한 초대 토큰이 코루틴 취소 시 유실됩니다.
consumePendingInvitation()이 즉시 저장소를 비운 뒤 loginScope(Login 컴포저블 스코프)에서 네트워크 호출을 기다립니다. 응답 전에 구성 변경/프로세스 재생성 등으로 컴포지션이 해제되면 코루틴이 취소되고, 토큰은 이미 소비되어 SavedStateHandle에도 남아있지 않아 초대 처리가 영구히 사라집니다(AuthenticationRequired 경로에서만 복원 중).
취소 시에도 토큰을 복원하거나, 결과 확정 후에 소비하도록 바꾸는 편이 안전합니다.
🛠️ 제안: 취소 시 토큰 복원
if (pendingInvitationToken.isNotBlank()) {
loginScope.launch {
- when (
- fileViewModel.receiveSharedFolderInvitation(
- pendingInvitationToken
- )
- ) {
+ val result = try {
+ fileViewModel.receiveSharedFolderInvitation(
+ pendingInvitationToken
+ )
+ } catch (e: CancellationException) {
+ // 취소되면 다음 진입에서 이어서 처리하도록 토큰을 되돌립니다.
+ deepLinkViewModel.setPendingInvitation(pendingInvitationToken)
+ throw e
+ }
+ when (result) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val pendingInvitationToken = | |
| deepLinkViewModel.consumePendingInvitation() | |
| if (pendingInvitationToken.isNotBlank()) { | |
| loginScope.launch { | |
| when ( | |
| fileViewModel.receiveSharedFolderInvitation( | |
| pendingInvitationToken | |
| ) | |
| ) { | |
| is AcceptSharedFolderInvitationResult.Accepted -> { | |
| openSharedFoldersFromLogin() | |
| } | |
| is AcceptSharedFolderInvitationResult.AcceptedButRefreshFailed -> { | |
| // 초대 수락은 완료되었으므로 갱신 실패를 알리고 공유 폴더를 엽니다. | |
| Toast.makeText( | |
| context, | |
| R.string.share_folder_refresh_failed, | |
| Toast.LENGTH_SHORT, | |
| ).show() | |
| openSharedFoldersFromLogin() | |
| } | |
| // TODO: 지민님 딥링크 대기 작업 처리 확인 필요 요청하기. | |
| is AcceptSharedFolderInvitationResult.AuthenticationRequired -> { | |
| // 소비한 토큰을 복원해 다음 로그인 성공 후 초대 수락을 재시도합니다. | |
| deepLinkViewModel.setPendingInvitation( | |
| pendingInvitationToken | |
| ) | |
| Toast.makeText( | |
| context, | |
| R.string.authentication_required, | |
| Toast.LENGTH_SHORT, | |
| ).show() | |
| navigateFromLoginTo(NavigationRoute.Login.route) | |
| } | |
| // 딥링크 대기 작업 처리 //지민아 이거 정리해줄 수 있어? | |
| deepLinkViewModel.consumePendingInvitation().let { token -> | |
| fileViewModel.receiveSharedFolderInvitation(token) | |
| folderStateViewModel.updateIsSharedFolders(true) | |
| is AcceptSharedFolderInvitationResult.InvalidInvitation -> { | |
| handlePendingInvitationFailure( | |
| R.string.invalid_share_link | |
| ) | |
| } | |
| navigator.navigate(NavigationRoute.File.route) { | |
| popUpTo("login_root") { inclusive = true } | |
| launchSingleTop = true | |
| is AcceptSharedFolderInvitationResult.NetworkFailure -> { | |
| handlePendingInvitationFailure( | |
| R.string.network_error | |
| ) | |
| } | |
| is AcceptSharedFolderInvitationResult.Failure -> { | |
| handlePendingInvitationFailure( | |
| R.string.undefined_behavior | |
| ) | |
| } | |
| } | |
| } | |
| return@LoginApp | |
| } | |
| val pendingInvitationToken = | |
| deepLinkViewModel.consumePendingInvitation() | |
| if (pendingInvitationToken.isNotBlank()) { | |
| loginScope.launch { | |
| val result = try { | |
| fileViewModel.receiveSharedFolderInvitation( | |
| pendingInvitationToken | |
| ) | |
| } catch (e: CancellationException) { | |
| // 취소되면 다음 진입에서 이어서 처리하도록 토큰을 되돌립니다. | |
| deepLinkViewModel.setPendingInvitation(pendingInvitationToken) | |
| throw e | |
| } | |
| when (result) { | |
| is AcceptSharedFolderInvitationResult.Accepted -> { | |
| openSharedFoldersFromLogin() | |
| } | |
| is AcceptSharedFolderInvitationResult.AcceptedButRefreshFailed -> { | |
| // 초대 수락은 완료되었으므로 갱신 실패를 알리고 공유 폴더를 엽니다. | |
| Toast.makeText( | |
| context, | |
| R.string.share_folder_refresh_failed, | |
| Toast.LENGTH_SHORT, | |
| ).show() | |
| openSharedFoldersFromLogin() | |
| } | |
| is AcceptSharedFolderInvitationResult.AuthenticationRequired -> { | |
| // 소비한 토큰을 복원해 다음 로그인 성공 후 초대 수락을 재시도합니다. | |
| deepLinkViewModel.setPendingInvitation( | |
| pendingInvitationToken | |
| ) | |
| Toast.makeText( | |
| context, | |
| R.string.authentication_required, | |
| Toast.LENGTH_SHORT, | |
| ).show() | |
| navigateFromLoginTo(NavigationRoute.Login.route) | |
| } | |
| is AcceptSharedFolderInvitationResult.InvalidInvitation -> { | |
| handlePendingInvitationFailure( | |
| R.string.invalid_share_link | |
| ) | |
| } | |
| is AcceptSharedFolderInvitationResult.NetworkFailure -> { | |
| handlePendingInvitationFailure( | |
| R.string.network_error | |
| ) | |
| } | |
| is AcceptSharedFolderInvitationResult.Failure -> { | |
| handlePendingInvitationFailure( | |
| R.string.undefined_behavior | |
| ) | |
| } | |
| } | |
| } | |
| return@LoginApp | |
| } |
🤖 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 417 - 474, Update the
pending-invitation handling around deepLinkViewModel.consumePendingInvitation()
and the loginScope.launch block so cancellation before the invitation result is
finalized restores the consumed token via
deepLinkViewModel.setPendingInvitation(pendingInvitationToken). Ensure
restoration occurs for coroutine cancellation, while preserving the existing
AuthenticationRequired restoration and all result-specific handling.
|
|
||
| data object Splash: NavigationRoute("splash") | ||
| data object Login: NavigationRoute("login") | ||
| data object Login: NavigationRoute("login_root") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP --type=kotlin -C2 '"login(_root)?"' -g '!**/build/**'Repository: LinkYou-2025/LinkU_Android
Length of output: 164
🏁 Script executed:
#!/bin/bash
set -u
echo "## matching kotlin files"
git ls-files '*NavigationDestination.kt' 'NavigationDestination.kt' | sed -n '1,100p'
echo
echo "## login-related Kotlin matches"
rg -n --type=kotlin -C2 'login|Login|navigationRoutes|NavigationRoute|popUpTo|deepLinks|startDestination|NavHost' -g '!**/build/**' | sed -n '1,240p'
echo
echo "## file context if present"
if [ -f app/src/main/java/com/linku/NavigationDestination.kt ]; then
cat -n app/src/main/java/com/linku/NavigationDestination.kt | sed -n '1,120p'
fiRepository: LinkYou-2025/LinkU_Android
Length of output: 275
🏁 Script executed:
#!/bin/bash
set -eu
# If the first search missed because of rg --type=kotlin behavior in this env,
# search tracked Kotlin files directly instead.
echo "## tracked Kotlin login matches"
git ls-files '*.kt' '*.kts' \
| xargs rg -n -C2 'login|Login|NavigationRoute|popupTo|popUpTo|deepLinks|navigate\(' \
| sed -n '1,260p'
echo
echo "## exact route/path string literals mentioning login"
git ls-files '*.kt' '*.kts' \
| xargs rg -n -C2 '"[^"]*login[^"]*"' \
| sed -n '1,260p'Repository: LinkYou-2025/LinkU_Android
Length of output: 41772
🏁 Script executed:
#!/bin/bash
set -eu
echo "## app NavHost / navigation-related Kotlin files"
git ls-files '*.kt' | xargs rg -l -n 'NavHost|navigation\(|auth_graph|LoginApp|LoginDestination|NavigationDestination|navigate(2\()?' app/src/main/java/com/linku feature/login/src/main/java/com/linku/login/navigation 2>/dev/null | sed -n '1,120p'
echo
echo "## relevant NavigationDestination imports/usages"
rg -n -C3 'NavigationDestination|NavigationRoute|LoginRoot|login_root|splash|home|file' app/src/main/java/com/linku feature/login/src/main/java/com/linku/login/navigation 2>/dev/null | sed -n '1,260p'
echo
echo "## imports for NavigateToLoginOnError callers/usages"
rg -n -C4 'NavigateToLoginOnError|LoginDestination\.|auth_graph|onNavigateToEmailLogin|navEntryHelper' app/src/main/java/com/linku feature/login/src/main/java/com/linku/login 2>/dev/null | sed -n '1,260p'Repository: LinkYou-2025/LinkU_Android
Length of output: 50382
login_root을 로그인 중첩 그래프의 "login"과 구별하세요.
MainApp에서 NavigationRoute.Login.route(login_root)로 이동하지만, auth_graph의 실제 목적지는 "login"이에요. NavigationRoute.Login.route를 계속 쓰면 LoginApp 기준으로 도달할 수 없으며, feature/login 내부의 "login" 라우트까지 의도한 변경인지 확인이 필요합니다.
🤖 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/NavigationDestination.kt` at line 21,
NavigationDestination.Login의 route 값이 auth_graph의 실제 목적지와 불일치합니다. MainApp의
NavigationRoute.Login 사용 흐름과 LoginApp의 중첩 그래프를 확인해, 진입 목적지는 "login"으로 맞추고
feature/login 내부의 "login" 라우트와 login_root가 혼동되지 않도록 별도 라우트 심볼을 유지하거나 참조를 올바르게
분리하세요.
| fun removeRecentQuery(searchHistoryId: Long) { | ||
| val requestId = ++historyRequestId | ||
| historyJob?.cancel() | ||
| historyJob = viewModelScope.launch { | ||
| _uiState.update { | ||
| it.copy( | ||
| isHistoryLoading = true, | ||
| errorMessage = null, | ||
| ) | ||
| } | ||
|
|
||
| try { | ||
| recentSearchRepository.remove(searchHistoryId) | ||
| .onSuccess { | ||
| if (historyRequestId == requestId) { | ||
| _uiState.update { state -> | ||
| state.copy( | ||
| recentQueries = state.recentQueries.filterNot { query -> | ||
| query.searchHistoryId == searchHistoryId | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| .onFailure { exception -> | ||
| if (historyRequestId == requestId) { | ||
| _uiState.update { it.copy(errorMessage = exception.message) } | ||
| } | ||
| } | ||
| } catch (exception: CancellationException) { | ||
| throw exception | ||
| } finally { | ||
| if (historyRequestId == requestId) { | ||
| _uiState.update { it.copy(isHistoryLoading = false) } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
연속 삭제 시 앞선 삭제 요청이 취소되어 유실될 수 있습니다.
removeRecentQuery가 공용 historyJob을 취소하기 때문에, 사용자가 칩 두 개를 빠르게 연달아 삭제하면 첫 번째 recentSearchRepository.remove() 코루틴이 응답 전에 취소됩니다. 서버 삭제가 반영되지 않을 수 있고, 로컬 recentQueries에서도 첫 항목이 제거되지 않은 채 남습니다. 삭제/전체삭제는 조회(loadRecentQueries)와 잡을 공유하지 말고 독립 실행하거나, 최소한 삭제 요청끼리는 취소되지 않게 분리하는 것이 좋습니다.
🐛 제안 방향
- fun removeRecentQuery(searchHistoryId: Long) {
- val requestId = ++historyRequestId
- historyJob?.cancel()
- historyJob = viewModelScope.launch {
+ fun removeRecentQuery(searchHistoryId: Long) {
+ viewModelScope.launch {(삭제 성공 시 로컬 상태만 갱신하고, 조회용 historyJob과는 분리)
🤖 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/search/SearchViewModel.kt` around lines 121 -
158, Update removeRecentQuery so it does not cancel or overwrite the shared
historyJob used by loadRecentQueries, allowing consecutive removals to run
independently. Preserve the existing requestId guards for stale UI updates,
while ensuring each successful removal filters its own searchHistoryId from
recentQueries and each request manages loading/error state without canceling
another deletion.
| assertTrue(client.interceptors.isEmpty()) | ||
| } | ||
|
|
||
| /** | ||
| * HTTP 로거를 제거한 뒤에도 인증 인터셉터와 큐레이션 전용 타임아웃이 유지되는지 검증합니다. | ||
| */ | ||
| @Test | ||
| fun `curation client keeps authentication interceptor and timeouts`() { | ||
| val client = createClient() | ||
|
|
||
| assertEquals(1, client.networkInterceptors.size) | ||
| assertEquals(TimeUnit.SECONDS.toMillis(60).toInt(), client.readTimeoutMillis) | ||
| assertEquals(TimeUnit.SECONDS.toMillis(15).toInt(), client.connectTimeoutMillis) | ||
| assertEquals(TimeUnit.SECONDS.toMillis(30).toInt(), client.writeTimeoutMillis) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# 실제 등록 위치와 인터셉터 종류를 확인합니다.
ast-grep outline data/src/main/java/com/linku/data/di/api/CurationApiModule.kt --items all
ast-grep outline data/src/main/java/com/linku/data/di/api/ServerApiModule.kt --items all
rg -n -C 3 'addInterceptor|addNetworkInterceptor|HttpLoggingInterceptor|Auth' \
data/src/main/java/com/linku/data/di/api/CurationApiModule.kt \
data/src/main/java/com/linku/data/di/api/ServerApiModule.ktRepository: LinkYou-2025/LinkU_Android
Length of output: 11628
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# 테스트 코드와 OkHttp Interceptor API 선언에 필요한 디렉터리/샘플 코드만 확인합니다.
sed -n '1,80p' data/src/test/java/com/linku/data/di/api/CurationApiModuleTest.kt
printf '\n--- ServerApiModuleTest ---\n'
sed -n '1,60p' data/src/test/java/com/linku/data/di/api/ServerApiModuleTest.kt
printf '\n--- OkHttp interceptors declaration in Gradle ---\n'
rg -n 'okhttp|buildDir|compileOnly|testImplementation|okhttp3' --glob '*.gradle*' --glob '*.kts' .Repository: LinkYou-2025/LinkU_Android
Length of output: 4373
🌐 Web query:
OkHttp OkHttpClient networkInterceptors interceptors API addNetworkInterceptor application interceptors
💡 Result:
In OkHttp, interceptors are a powerful mechanism for observing, modifying, and potentially short-circuiting HTTP requests and responses [1][2]. You register them using OkHttpClient.Builder via two distinct methods, each serving different purposes [1][2]. Application Interceptors Added via addInterceptor(), these are the first point of contact for a request [1][2]. - Scope: They observe the application's original intent [1]. - Behavior: They are invoked exactly once, even if the response is served from the cache or involves multiple redirects/retries [1][2]. - Use Case: Ideal for adding authentication headers, logging high-level business events, or modifying requests/responses without worrying about low-level network details [2]. - Flexibility: They are permitted to short-circuit the chain (not call proceed) or retry the request [1]. Network Interceptors Added via addNetworkInterceptor(), these operate at the low level of the network layer [1][3]. - Scope: They observe data exactly as it is transmitted over the network [1][2]. - Behavior: They are invoked only for network transmissions [1][2]. They are not called if a request is served from the cache [1][2]. Conversely, they may be called multiple times during redirects or retries [1][2]. - Use Case: Best for monitoring network-level details, such as inspecting raw headers, connection state (IP/TLS configuration), or observing how data is actually sent [1][2]. - Constraints: They must call Interceptor.Chain.proceed() exactly once [4][5]. Short-circuiting or repeating a request is considered an error at this level [4][6]. In summary, use Application interceptors for application-level concerns (like auth or business logging) and Network interceptors for low-level concerns (like raw wire logging or connection monitoring) [2][3].
Citations:
- 1: https://square.github.io/okhttp/features/interceptors/
- 2: https://www.androidsystemdesign.dev/blog/okhttp-interceptor-android
- 3: https://stackoverflow.com/questions/37119429/android-retrofit-2-differences-between-addinterceptor-addnetworkinterceptor-f
- 4: https://square.github.io/okhttp/5.x/okhttp/okhttp3/-ok-http-client/-builder/network-interceptors.html
- 5: https://square.github.io/okhttp/5.x/okhttp/okhttp3/-ok-http-client/network-interceptors.html
- 6: https://github.com/square/okhttp/blob/fce60e5a09eac198a0f9c676e486163212662230/okhttp/src/main/kotlin/okhttp3/OkHttpClient.kt
🌐 Web query:
OkHttp logging interceptor addNetworkInterceptor response.body string
💡 Result:
In OkHttp, the response body is a "one-shot" stream, meaning it can only be consumed once [1]. If you attempt to call response.body.string to log the content inside an interceptor, you consume the stream, and the downstream application (which expects to read the body later) will receive an empty or closed body, often resulting in an IllegalStateException [2]. To safely log the response body within an interceptor without breaking the flow, use one of the following approaches: 1. Use peekBody(long byteCount): The peekBody method allows you to create a copy of the response body that you can consume for logging without affecting the original response [3]. It buffers the specified number of bytes into memory. Response response = chain.proceed(chain.request); ResponseBody peekedBody = response.peekBody(Long.MAX_VALUE); String bodyString = peekedBody.string; // Log bodyString here return response; 2. Replace the Response Body: If you need to perform modifications or require the body to be read fully, you can consume the original body, read it into a string, and then construct a new Response object with a new ResponseBody created from that string [2][3]. Response response = chain.proceed(chain.request); String bodyString = response.body.string; // Log bodyString here ResponseBody newBody = ResponseBody.create(response.body.contentType, bodyString); return response.newBuilder.body(newBody).build; Important Considerations: - Memory Usage: Reading the entire body into a string consumes memory proportional to the size of the response. This can lead to OutOfMemoryErrors for large payloads [2]. The official HttpLoggingInterceptor mitigates this by applying limits and checks [4][5]. - Network Interceptors: When using addNetworkInterceptor, remember that it is called for every network transmission (including redirects and retries) [6][7]. It sees raw data (e.g., gzipped streams), whereas Application interceptors (addInterceptor) see the final, decoded data [6][7]. If you must log the body via a Network Interceptor, you may need to handle Gzip decompression manually if the content is compressed [8][9]. - Official Interceptor: For production environments, it is highly recommended to use the official com.squareup.okhttp3:logging-interceptor artifact rather than implementing a custom one, as it is designed to handle these edge cases and security concerns efficiently [10][7].
Citations:
- 1: https://square.github.io/okhttp/5.x/okhttp/okhttp3/-response-body/index.html
- 2: https://stackoverflow.com/questions/38641565/okhttp-throwing-an-illegal-state-exception-when-i-try-to-log-the-network-respons
- 3: https://stackoverflow.com/questions/73825701/decoding-response-body-on-the-fly-in-an-okhttp-interceptor
- 4: https://github.com/square/okhttp/blob/master/okhttp-logging-interceptor/src/main/kotlin/okhttp3/logging/HttpLoggingInterceptor.kt
- 5: https://github.com/square/okhttp/blob/okhttp_3.14.x/okhttp-logging-interceptor/src/main/java/okhttp3/logging/HttpLoggingInterceptor.java
- 6: https://square.github.io/okhttp/features/interceptors/
- 7: https://www.androidsystemdesign.dev/blog/okhttp-interceptor-android
- 8: https://gist.github.com/erickok/e371a9e0b9e702ed441d
- 9: https://github.com/square/okhttp/blob/okhttp_3.13.x/okhttp-logging-interceptor/src/main/java/okhttp3/logging/HttpLoggingInterceptor.java
- 10: https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor
인터셉터 개수만으로는 로깅 제거·인증 유지 계약을 보장할 수 없습니다.
addNetworkInterceptor()는 OkHttpClient.interceptors 대상이 아니라 networkInterceptors 대상이라 interceptors.isEmpty() 검사만으로는 원문 로거가 네트워크 계층에 남아 있어도 통과할 수 있습니다. 또한 networkInterceptors.size == 1은 해당 인터셉터가 인증 설정을 유지한다는 뜻이 아닙니다.
data/src/test/java/com/linku/data/di/api/CurationApiModuleTest.kt#L23-L36:networkInterceptors에서 애플리케이션 로거가 없음과 인증 인터셉터가 등록되어 있음을 함께 검증하세요.data/src/test/java/com/linku/data/di/api/ServerApiModuleTest.kt#L20-26: 공개 클라이언트의networkInterceptors에도 로거가 없음을 함께 검증하세요.
📍 Affects 2 files
data/src/test/java/com/linku/data/di/api/CurationApiModuleTest.kt#L23-L36(this comment)data/src/test/java/com/linku/data/di/api/ServerApiModuleTest.kt#L20-L26
🤖 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/test/java/com/linku/data/di/api/CurationApiModuleTest.kt` around
lines 23 - 36, The client tests only check interceptor counts, so they do not
verify logger removal or authentication registration. In
data/src/test/java/com/linku/data/di/api/CurationApiModuleTest.kt lines 23-36,
update curation client assertions to inspect networkInterceptors, confirm the
application logger is absent, and verify the authentication interceptor is
present while preserving timeout checks. In
data/src/test/java/com/linku/data/di/api/ServerApiModuleTest.kt lines 20-26,
also assert that the public client’s networkInterceptors contain no logger.
| uiState.errorMessage?.let { errorMessage -> | ||
| Text( | ||
| text = "찾으시는 검색어 결과가 없어요!", | ||
| modifier = Modifier | ||
| .align(Alignment.BottomCenter) | ||
| .padding(bottom = 22.dp), | ||
| text = errorMessage, | ||
| fontSize = 13.sp, | ||
| color = Basic.negative | ||
| color = Basic.negative, | ||
| ) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
예외 원문 메시지를 그대로 사용자에게 노출합니다.
uiState.errorMessage는 SearchViewModel에서 exception.message를 그대로 담고 있어(HTTP 상태/스택 문구 등) 현지화되지 않은 기술 메시지가 화면에 표시됩니다. 다른 오류 표시처럼 stringResource 기반 고정 문구로 바꾸고, 원문은 로그로만 남기는 편이 좋습니다.
🤖 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 `@design/src/main/java/com/linku/design/top/search/SearchBarTopSheet.kt` around
lines 625 - 634, Update the error display around uiState.errorMessage to use a
localized fixed string via stringResource instead of rendering the exception
message directly. In SearchViewModel, retain the original exception details only
in logging and stop storing them as the user-facing error message; preserve the
existing error-state flow.
| is LoadState.NotLoading -> { | ||
| if (lazySearchResults.itemCount == 0) { | ||
| // "찾으시는 검색어 결과가 없어요!" 공간 | ||
| Row( | ||
| modifier = Modifier | ||
| .align(Alignment.BottomCenter) | ||
| .padding(bottom = 22.dp), | ||
| horizontalArrangement = Arrangement.spacedBy(8.dp), | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ){ | ||
| // "찾으시는 검색어 결과가 없어요!" 아이콘 | ||
| Icon( | ||
| modifier = Modifier.size(20.dp), | ||
| painter = painterResource(R.drawable.ic_search_bar_caution), | ||
| contentDescription = stringResource( | ||
| R.string.search_results_empty_icon_description | ||
| ), | ||
| tint = Basic.negative | ||
| ) | ||
|
|
||
| // "찾으시는 검색어 결과가 없어요!" 텍스트 | ||
| Text( | ||
| text = stringResource(R.string.search_results_empty), | ||
| fontSize = 13.sp, | ||
| color = Basic.negative | ||
| ) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
최소 검색어 길이(2자) 미만 입력에서도 "결과 없음"이 표시됩니다.
SearchViewModel은 쿼리가 2자 미만이면 PagingData.empty()를 흘려보내므로, 사용자가 한 글자만 입력한 시점에 refresh는 NotLoading, itemCount == 0이 되어 "찾으시는 검색어 결과가 없어요!"가 노출됩니다. 또한 디바운스(800ms) 대기 중에도 이전 상태 기준으로 같은 문구가 먼저 보입니다. 최소 길이 미만일 때는 안내 문구를 숨기거나 별도 안내(예: "2자 이상 입력해주세요")로 분기해 주세요.
🤖 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 `@design/src/main/java/com/linku/design/top/search/SearchBarTopSheet.kt` around
lines 676 - 702, Update the empty-result branch in the SearchBarTopSheet
composable to check the current search query length before rendering the “search
results empty” Row. For queries shorter than two characters, hide that message
or show the existing minimum-length guidance instead; preserve the current
empty-results message for queries meeting the minimum length and prevent stale
results during the debounce interval from displaying it.
| val localProperties = Properties().apply { | ||
| val file = rootProject.file("local.properties") | ||
| if (file.exists()) load(file.inputStream()) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
ast-grep outline feature/file/build.gradle.kts --lang kotlin --items all
rg -n -C2 'file\.inputStream\(\)' feature/file/build.gradle.ktsRepository: LinkYou-2025/LinkU_Android
Length of output: 461
🏁 Script executed:
#!/bin/bash
cat -n feature/file/build.gradle.ktsRepository: LinkYou-2025/LinkU_Android
Length of output: 4107
local.properties 읽기 스트림을 use 블록으로 닫으세요.
file.inputStream()의 반환값을 즉시 사용하지 않고 Properties.load()에 넘기면 스트림이 닫히지 않아 Gradle 구성 반복 시 파일 디스크립터가 누수될 수 있습니다.
수정 예시
- if (file.exists()) load(file.inputStream())
+ if (file.exists()) file.inputStream().use { load(it) }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val localProperties = Properties().apply { | |
| val file = rootProject.file("local.properties") | |
| if (file.exists()) load(file.inputStream()) | |
| } | |
| val localProperties = Properties().apply { | |
| val file = rootProject.file("local.properties") | |
| if (file.exists()) file.inputStream().use { load(it) } | |
| } |
🤖 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/build.gradle.kts` around lines 15 - 18, Update the
localProperties initialization block to wrap the file.inputStream() passed to
Properties.load() in a use block, ensuring the stream is closed after loading
local.properties while preserving the existing file-existence check and property
loading behavior.
| @@ -1,9 +1,7 @@ | |||
| package com.linku.file | |||
|
|
|||
| import android.net.Uri | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
ViewModel에 android.net.Uri 프레임워크 의존성 도입.
buildInvitationLink/extractInvitationToken에서 android.net.Uri를 직접 import/사용하고 있습니다. 이는 ViewModel이 Android 프레임워크 의존성을 갖지 않도록 하는 코딩 가이드라인에 어긋나며, 순수 JVM 단위 테스트를 어렵게 만듭니다. 이 링크 정규화 로직은 data/domain 계층의 순수 Kotlin 헬퍼로 옮기는 것을 권장합니다.
As per coding guidelines, "**/*ViewModel.kt: ViewModel classes must not reference android.content.Context or introduce Android framework dependencies."
Also applies to: 1097-1129
🤖 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 3, Remove
the direct android.net.Uri dependency from the ViewModel methods
buildInvitationLink and extractInvitationToken. Move their link parsing and
normalization logic into a pure Kotlin helper in the data or domain layer, then
have the ViewModel delegate to that helper while preserving existing behavior
and enabling JVM unit testing.
Source: Coding guidelines
KateteDeveloper
left a comment
There was a problem hiding this comment.
바쁨 이슈로 믿음의 어프루브 합니다~!
LGTM👍👍
| private var pendingInvitationToken: String | ||
| get() = savedStateHandle[PENDING_INVITATION_TOKEN_KEY] ?: "" | ||
| set(value) { | ||
| savedStateHandle[PENDING_INVITATION_TOKEN_KEY] = value | ||
| } |
There was a problem hiding this comment.
여기서 이해가 가지 않는 부분이 있습니다!
초대 토큰이 값이 아예 없는 경우와 빈 문자열이 저장되는 것을 구분할 수 없을 것 같습니다! 물론 코드에서 지금 괜찮은 이유는 consumePendingInvitation: 소비 후 항상 ""로 리셋하는 것으로 보입니다:) 위의 코드와 통일성을 위해서는 아예 String?으로 작성하고 null체크하는 방식 (+SavedStateHandle.remove이 추후 수정 및 코드 읽기 수월할 것 같은데 어떻게 생각하실까요?
| private companion object { | ||
| const val PENDING_SHARE_FOLDER_ID_KEY = "pendingShareFolderId" | ||
| const val PENDING_INVITATION_TOKEN_KEY = "pendingInvitationToken" | ||
| } |
There was a problem hiding this comment.
이거 상수라 외부에서 접근할 일이 없으니 아예 딥링크핸들러뷰모델 바깥으로 빼는건 어때용?
codebidoof
left a comment
There was a problem hiding this comment.
이거 내가 연동할 때 하면 될 듯해~ 이 부분은 롤백 부탁해용
| /** | ||
| * [CurationApiModule]의 보안 및 네트워크 구성 계약을 검증합니다. | ||
| */ | ||
| class CurationApiModuleTest { | ||
|
|
||
| /** | ||
| * 큐레이션 클라이언트가 HTTP 원문을 기록할 애플리케이션 인터셉터를 등록하지 않는지 검증합니다. | ||
| */ | ||
| @Test | ||
| fun `curation client does not register application interceptors`() { | ||
| val client = createClient() | ||
|
|
||
| assertTrue(client.interceptors.isEmpty()) | ||
| } | ||
|
|
||
| /** | ||
| * HTTP 로거를 제거한 뒤에도 인증 인터셉터와 큐레이션 전용 타임아웃이 유지되는지 검증합니다. | ||
| */ | ||
| @Test | ||
| fun `curation client keeps authentication interceptor and timeouts`() { | ||
| val client = createClient() | ||
|
|
||
| assertEquals(1, client.networkInterceptors.size) | ||
| assertEquals(TimeUnit.SECONDS.toMillis(60).toInt(), client.readTimeoutMillis) | ||
| assertEquals(TimeUnit.SECONDS.toMillis(15).toInt(), client.connectTimeoutMillis) | ||
| assertEquals(TimeUnit.SECONDS.toMillis(30).toInt(), client.writeTimeoutMillis) | ||
| } | ||
|
|
||
| /** | ||
| * 네트워크 요청을 실행하지 않는 구성 테스트용 큐레이션 클라이언트를 생성합니다. | ||
| * | ||
| * @return 로거 없이 인증과 타임아웃 정책만 적용된 큐레이션 클라이언트 | ||
| */ | ||
| private fun createClient(): OkHttpClient = | ||
| CurationApiModule.provideCurationOkHttpClient( | ||
| authPreference = createUnusedAuthPreference() | ||
| ) | ||
|
|
||
| /** | ||
| * 클라이언트 구성 과정에서 호출되지 않아야 하는 [AuthPreference] 대역을 생성합니다. | ||
| * | ||
| * @return 접근 시 즉시 실패하여 예기치 않은 인증 정보 조회를 드러내는 대역 | ||
| */ | ||
| private fun createUnusedAuthPreference(): AuthPreference = | ||
| Proxy.newProxyInstance( | ||
| AuthPreference::class.java.classLoader, | ||
| arrayOf(AuthPreference::class.java) | ||
| ) { _, method, _ -> | ||
| error("구성 단계에서 AuthPreference.${method.name} 호출을 예상하지 않았습니다.") | ||
| } as AuthPreference | ||
| } |
There was a problem hiding this comment.
이거 내가 연동할 때 하면 될 듯해~ 이 부분은 롤백 부탁해용
| } No newline at end of file | ||
| ): CurationApi = | ||
| retrofit.create(CurationApi::class.java) | ||
| } |
There was a problem hiding this comment.
이거 내가 연동할 때 하면 될 듯해~ 이 부분은 롤백 부탁해용
KateteDeveloper
left a comment
There was a problem hiding this comment.
파일 탑바부터는 믿음으로 갑시다
| } | ||
| .cachedIn(viewModelScope) | ||
|
|
||
| private var historyJob: Job? = null |
There was a problem hiding this comment.
[Q] 최근 검색어 조회 api가 있는데 이게 오히려 과한 코드이지 않을까?
-> 아 이거 있는 이유 찾아냄 로딩중에도 버튼 클릭이 가능하네 그러면 아예 버튼 쪽에 enabled = !isHistoryLoading도 같이 넣어서 애초에 중복 요청 자체를 막는건?
| val requestId = ++historyRequestId | ||
| historyJob?.cancel() | ||
| historyJob = viewModelScope.launch { | ||
| _uiState.update { | ||
| it.copy( | ||
| isHistoryLoading = true, | ||
| errorMessage = null, | ||
| ) | ||
| } |
There was a problem hiding this comment.
아 코드 반복이 많은데 아예 공통 함수로 빼는 건 어때...????????
private fun <T> runHistoryAction(
action: suspend () -> Result<T>,
onSuccess: (SearchBarUiState, T) -> SearchBarUiState,
) {
val requestId = ++historyRequestId
historyJob?.cancel()
historyJob = viewModelScope.launch {
_uiState.update { it.copy(isHistoryLoading = true, errorMessage = null) }
try {
action()
.onSuccess { data ->
if (historyRequestId == requestId) _uiState.update { onSuccess(it, data) }
}
.onFailure { exception ->
if (historyRequestId == requestId) _uiState.update { it.copy(errorMessage = exception.message) }
}
} catch (e: CancellationException) {
throw e
} finally {
if (historyRequestId == requestId) _uiState.update { it.copy(isHistoryLoading = false) }
}
}
}
fun loadRecentQueries() = runHistoryAction(recentSearchRepository::getRecentQueries) { state, queries ->
state.copy(recentQueries = queries.map { RecentSearchItem(it.searchHistoryId, it.keyword) })
}
fun removeRecentQuery(searchHistoryId: Long) =
runHistoryAction({ recentSearchRepository.remove(searchHistoryId) }) { state, _ ->
state.copy(recentQueries = state.recentQueries.filterNot { it.searchHistoryId == searchHistoryId })
}
fun clearRecentQueries() = runHistoryAction(recentSearchRepository::clear) { state, _ ->
state.copy(recentQueries = emptyList())
}이렇게!!
| _uiState.value = SearchBarUiState() | ||
| } | ||
|
|
||
| private companion object { |
| /** | ||
| * SystemBarController 구현 | ||
| * 앱 전역 시스템 바 단일 제어 지점 | ||
| * 앱 전역 시스템 바 표시 상태와 콘텐츠의 시스템 윈도우 맞춤 여부를 함께 적용합니다. |
| * | ||
| * @param mode 적용할 시스템 바 표시 모드 | ||
| */ | ||
| override fun setSystemBarMode(mode: SystemBarMode) { |
There was a problem hiding this comment.
아저씨 이거 아예 확 지우는거 어때? 차라리 MainScreen이 이미 edgeToEdgeSystemBars 상태 기반으로 EdgeToEdgeSystemBars를 중앙에서 호출해 바 표시/아이콘 밝기를 처리하고 있고(MainScreen.kt) -> 제가 했었음... 그래서 이거 쓰는 곳은 3곳이니 3곳도 수정하면 아예 이거 걷어낼 수 있을 듯?
여기서 부터는 클선생의 정리....1. HomeScreen.kt:94-102
systemBarController.setSystemBarMode(SystemBarMode.VISIBLE) 삭제 — Home 진입 전 edgeToEdgeSystemBars = false로 이미 바뀌어 있어서(MainApp.kt:310), MainScreen의 EdgeToEdgeSystemBars가 알아서 바를 보여주고 있어요. 실제로 MyPageScreen.kt엔 이런 코드가 아예 없는데도 잘 동작하는 걸 확인했어요.
-
EmailLoginScreen.kt:82-91
동일한 이유로 systemBarController.setSystemBarMode(SystemBarMode.VISIBLE) 삭제 — LoginApp의 onEdgeToEdgeChange 콜백으로 이미 edgeToEdgeSystemBars가 관리되고 있어서 중복이에요. -
Splash.kt:45-53
여긴 나머지 둘과 달라요 — edgeToEdgeSystemBars = true인 동안 MainScreen이 바 관리를 일부러 스킵하기 때문에(MainScreen.kt:68 if (applyDefaultSystemBarIcons)), 여기서만 바를 숨기는 로직이 실제로 필요해요. 다만 SystemBarController를 지우니까, 대신 DesignSystemBars(immersive = true)로 바꿔서 유지해주시면 될 것 같아요.
3곳 다 반영되면 MainActivity의 setSystemBarMode랑 SystemBarController 인터페이스는 쓰는 곳이 없어져서 삭제 가능해요.
-> 내가 하고 싶은데 차라리 이거 지금 주석단 김에 해줘. 해줘. 해줘. 해줘. SystemBarController 이것도 지워죠><
| } | ||
|
|
||
| // TODO: 지민님 딥링크 대기 작업 처리 확인 필요 요청하기. | ||
| is AcceptSharedFolderInvitationResult.AuthenticationRequired -> { |
There was a problem hiding this comment.
[Q]invitationLinkRoute()에서 이미 쓰고 있지 않아??
클선생의 첨언을 붙입니다:)
"이 when(result) 분기, invitationLinkRoute()(InvitationLinkRoute.kt)가 이미 똑같이 처리하고 있는데 여기서 왜 따로 구현했는지 궁금해요. invitationLinkRoute(token = pendingInvitationToken, isLoggedIn = true, onReceiveSharedFolderInvitation = fileViewModel::receiveSharedFolderInvitation, ...)로 재사용하면 중복도 없애고 deepLinkEntryId 기반의 stale-result 가드도 같이 얻어갈 수 있을 것 같아요.
| invitationLinkRoute( | ||
| token = token, | ||
| isLoggedIn = viewModel.hasValidRefreshToken(), | ||
| onReceiveSharedFolderInvitation = fileViewModel::receiveSharedFolderInvitation, | ||
| onUpdateIsSharedFolders = { isSharedFolders -> | ||
| folderStateViewModel.resetSharedFolderState() | ||
| folderStateViewModel.updateIsSharedFolders( | ||
| isSharedFolders | ||
| ) | ||
| }, | ||
| onSetPendingInvitation = deepLinkViewModel::setPendingInvitation, | ||
| onInvalidLink = { | ||
| Toast.makeText( | ||
| context, | ||
| R.string.invalid_share_link, | ||
| Toast.LENGTH_SHORT | ||
| ).show() | ||
| }, | ||
| onAuthenticationRequired = { | ||
| Toast.makeText( | ||
| context, | ||
| R.string.authentication_required, | ||
| Toast.LENGTH_SHORT | ||
| ).show() | ||
| }, | ||
| onNetworkFailure = { | ||
| Toast.makeText( | ||
| context, | ||
| R.string.network_error, | ||
| Toast.LENGTH_SHORT | ||
| ).show() | ||
| }, | ||
| onRefreshFailed = { | ||
| Toast.makeText( | ||
| context, | ||
| R.string.share_folder_refresh_failed, | ||
| Toast.LENGTH_SHORT | ||
| ).show() | ||
| }, | ||
| onFailure = { | ||
| Toast.makeText( | ||
| context, | ||
| R.string.undefined_behavior, | ||
| Toast.LENGTH_SHORT | ||
| ).show() | ||
| }, | ||
| navigator = navigator, | ||
| deepLinkEntryId = backStackEntry.id, | ||
| ) | ||
|
|
||
| } catch (e: CancellationException) { | ||
| // Compose effect 취소를 일반 오류로 변환하지 않고 구조화된 취소를 전파합니다. | ||
| throw e | ||
|
|
||
| } catch (e: DeepLinkError.MissingInvitationToken) { | ||
| // 토큰 누락은 현재 화면과 초대 상태를 바꾸지 않고 안내만 표시합니다. | ||
| Toast.makeText( | ||
| context, | ||
| R.string.invalid_share_link, | ||
| Toast.LENGTH_SHORT | ||
| ).show() | ||
|
|
||
| } catch (e: IllegalArgumentException) { | ||
| // 추후 공통 토스트 메시지로 변경 | ||
| Toast.makeText(context, R.string.undefined_behavior, Toast.LENGTH_SHORT).show() | ||
|
|
||
| } catch (e: Exception) { | ||
| // 추후 공통 토스트 메시지로 변경 | ||
| Toast.makeText(context, R.string.undefined_behavior, Toast.LENGTH_SHORT).show() | ||
| } |
There was a problem hiding this comment.
이것두 위랑 같은 이슈라구 생각합니다!!
| <resources> | ||
| <string name="invalid_share_link">유효하지 않은 공유 링크입니다.</string> | ||
| <string name="undefined_behavior">예상치 못한 오류가 발생했습니다.</string> | ||
| <string name="authentication_required">로그인이 필요한 공유 링크입니다.</string> |
|
|
||
| val acceptedFolderId = try { | ||
| invitationRepository.acceptInvitation(token) | ||
| } catch (e: CancellationException) { |
There was a problem hiding this comment.
이거 다같이 쓰면 좋을듯? 아예 전체 공통확장함수로 뽑아서 다같이 쓰게?? 아 그치만 그러면 내가 유스케이스까지 리펙해야하잖아 지금 좋은 것 같아요 최고최고
| * @param locate 로그 메시지 앞에 표시할 위치 정보 | ||
| * @param tr 그 메시지와 함께 스택 트레이스를 출력할 [Throwable] 객체 | ||
| * @param msg 로그로 출력할 지연 생성 메시지 | ||
| */ |
There was a problem hiding this comment.
아저씨.. 로그가 너무 광기 아닐까??? 동일한 확장함수가 너무 많으면 헷갈릴 것 같은데 딱 쓸것만 몇개만 남기고 우리 런칭 전에 코덱스로 싹 바꾸는게 좋을 것 같은데? 아닌가
일단 진정해
📝 설명
SearchViewModel을 Home/File 화면에 연결했습니다.open?token=...형식으로 통일하고 레거시linku://open스킴을 제거했습니다.develop통합 과정에서 발생한 Compose 상태 수집 import,SERVER_HOST진단 메시지와 테스트 fake의 Repository 계약을 보정했습니다.영향 및 확인 사항
local.properties에 scheme/path가 없는SERVER_HOST값이 필요합니다.linku://open링크는 더 이상 처리하지 않으므로 HTTPS App Link와assetlinks.json연결 상태를 배포 환경에서 확인해야 합니다.검증
git diff --check origin/develop...HEAD:feature:file:compileDebugKotlin:feature:home:compileDebugKotlin:app:compileDebugKotlin:app:testDebugUnitTest— 22개 통과:core:testDebugUnitTest— 12개 통과:data:testDebugUnitTest전체 — 25개 중 17개 통과,origin/develop에도 동일한 기존 8개 실패FolderMapperTest4개: JVM 테스트에서android.util.Log가 mock되지 않음StringExtTest4개:Z가 붙은 UTC 문자열과LocalDateTime.parse()구현 불일치✔️ PR 유형
어떤 변경 사항이 있나요?
📎 관련 이슈 번호
Summary by CodeRabbit
새 기능
버그 수정