-
Notifications
You must be signed in to change notification settings - Fork 1
SCRUM-243 SCRUM-244 SCRUM-245 feature: kakao login and server login integration #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
gdaegeun539
merged 59 commits into
project-lyrics:develop
from
gdaegeun539:feature/SCRUM-243-SCRUM-244-SCRUM-245
Apr 18, 2026
Merged
Changes from all commits
Commits
Show all changes
59 commits
Select commit
Hold shift + click to select a range
8715b4d
init: add kakao login sdk
gdaegeun539 a4b3b1d
feat: implement kakao auth data source
gdaegeun539 0af1915
chore: change gradlew permission to 755
gdaegeun539 9e6b7e2
chore: modify ai agent rules
gdaegeun539 c15a24c
feat: add token models
gdaegeun539 40b7f8b
feat: covert kakao oauth token to common model
gdaegeun539 b1c1bc2
ci: exclude TooManyFunctions rule at datasource
gdaegeun539 b44658b
chore: modify ai agent rules
gdaegeun539 14a897c
build: Add DataStore dependency for token persistence
gdaegeun539 d80b745
feat: Add string parsing to OAuthProvider enum
gdaegeun539 a47b341
feat: Add AuthLocalDataSource for token persistence
gdaegeun539 4d642a8
feat: Add AuthManager for centralized auth state
gdaegeun539 7fe87b4
feat: Add AuthInterceptor for automatic token injection
gdaegeun539 761a8e1
feat: Integrate AuthManager into AuthRepository
gdaegeun539 b274429
feat: Add Hilt dependency injection module
gdaegeun539 00a7dc3
feat: move profile image type to domain
gdaegeun539 f506344
init: add serializer, okhttp logger
gdaegeun539 fe80d5e
feat: add auth api service, model, server dto
gdaegeun539 d2bc19c
feat: implement jwt rtr authenticator logic
gdaegeun539 3071e8f
feat: set okhttp, retrofit, api service
gdaegeun539 d7957e1
ci: exclude authenticator retruncount check
gdaegeun539 b19923a
feat: modify reissue token api request body
gdaegeun539 85ec761
feat: modify signIn, signUp api service
gdaegeun539 70b4824
feat: add httpexception to serverdto converter
gdaegeun539 0c6e10c
feat: add own server exception
gdaegeun539 4937bde
feat: allow authmanager save own server jwt only
gdaegeun539 41dbaa8
feat: implement server remote source
gdaegeun539 a6aaeef
feat: connect remote source to auth repository
gdaegeun539 311d13b
refactor: remove unused annotation in auth repo
gdaegeun539 3283ef7
fix: fix serializer version typo
gdaegeun539 5e02a6d
fix: fix profiletype import path
gdaegeun539 ee2fc96
init: hilt plugin, application setting
gdaegeun539 6ad2758
fix: datasource hilt binding fix
gdaegeun539 cc2c9bc
feat: di loginViewModel with hilt
gdaegeun539 a77772c
refactor: utilize kakao token convertor
gdaegeun539 20b85b5
refactor: remove context from kakao auth source
gdaegeun539 e23c1ff
refactor: let login view integrate login sdk
gdaegeun539 b7a7b98
feat: allow sever connection to http
gdaegeun539 ad35758
feat: add app version, logging interceptor
gdaegeun539 2694422
docs: social login architecture helper doc add
gdaegeun539 4e63bfd
refactor: fix missing newline issue
gdaegeun539 6d3138c
Merge branch 'develop' into feature/SCRUM-243-SCRUM-244-SCRUM-245
gdaegeun539 723f693
fix: resolve merge error
gdaegeun539 3d0d3a6
fix: align login result state with navigation
gdaegeun539 a27b5ec
feat: show server error at login screen
gdaegeun539 2c3298a
build: change server url
gdaegeun539 0946911
ci: adjust detekt's max return count 2 to 3
gdaegeun539 dca7f79
refactor: Simplify authentication failure handling
gdaegeun539 8fe9ea0
chore: remove legacy ai agent guidelines
gdaegeun539 db549a1
chore: fix typo
gdaegeun539 68b6a9c
fix: throw build error when kakao key is missing
gdaegeun539 6eadc52
fix: remove datasource provider class
gdaegeun539 bb9a4fe
fix: Reduce unsafe network logging exposure
gdaegeun539 1a6797b
fix: Prevent repeated JWT refresh retries
gdaegeun539 c7728c6
fix: prevent key lack issue at CI
gdaegeun539 60c2b53
fix: Support CI fallback for Kakao app key
gdaegeun539 65e8ee4
fix: Handle HttpException fallback parsing safely
gdaegeun539 a97de6d
fix: Show Login Error dialog safely
gdaegeun539 0a2491c
fix: Prevent empty JWT requests at startup
gdaegeun539 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,296 @@ | ||
| # AI Agent Development Guidelines | ||
|
|
||
| ## 📝 Git 커밋 메시지 규칙 | ||
|
|
||
| ### 핵심 원칙 | ||
|
|
||
| 좋은 Git 커밋 메시지는 코드 변경사항의 **맥락(context)**을 전달하는 가장 효과적인 방법입니다. diff는 **무엇이** 바뀌었는지 보여주지만, 커밋 메시지만이 **왜** 바뀌었는지 설명할 수 있습니다. | ||
|
|
||
| ### ⚠️ 중요: 영문 작성 필수 | ||
|
|
||
| **모든 커밋 메시지는 반드시 영문으로 작성해야 합니다.** | ||
|
|
||
| - 국제적인 협업과 코드베이스 일관성을 위해 영문 사용 | ||
| - 기술 용어의 명확한 전달 | ||
| - Git 히스토리의 표준화 | ||
|
|
||
| ### 팀 커밋 접두사 컨벤션 | ||
|
|
||
| 변경사항의 유형을 명확히 하기 위해 다음 접두사를 사용합니다: | ||
|
|
||
| - `init:` - Initial setup | ||
| - `feat:` - New feature | ||
| - `docs:` - Documentation changes | ||
| - `build:` - Build system or dependency changes | ||
| - `design:` - UI/UX design changes | ||
| - `fix:` - Bug fixes | ||
| - `chore:` - Other minor changes | ||
| - `refactor:` - Code refactoring | ||
| - `ci:` - CI configuration changes (e.g., detekt.yml, GitHub Actions) | ||
| - `test:` - Test code changes | ||
|
|
||
| **Format**: `prefix: message` (lowercase prefix + colon + space + message) | ||
|
|
||
| **Examples**: | ||
|
|
||
| ``` | ||
| feat: Add Kakao login feature | ||
| fix: Resolve token refresh error | ||
| ci: Optimize detekt rules | ||
| ``` | ||
|
gdaegeun539 marked this conversation as resolved.
|
||
|
|
||
| ### 7가지 필수 규칙 | ||
|
|
||
| #### 1. 제목과 본문을 빈 줄로 분리 | ||
|
|
||
| ``` | ||
| Subject: Summary of changes | ||
|
|
||
| Body: Detailed explanation (if needed) | ||
| ``` | ||
|
|
||
| #### 2. 제목을 50자로 제한 | ||
|
|
||
| - Recommended: Within 50 characters | ||
| - Hard limit: 72 characters | ||
|
|
||
| #### 3. 제목의 첫 글자를 대문자로 작성 | ||
|
|
||
| ``` | ||
| ✅ feat: Add user authentication | ||
| ✅ fix: Resolve login validation bug | ||
|
|
||
| ❌ feat: add user authentication | ||
| ❌ fix: resolved login validation bug | ||
| ``` | ||
|
|
||
| #### 4. 제목 끝에 마침표 사용 금지 | ||
|
|
||
| ``` | ||
| ✅ fix: Login validation bug | ||
| ❌ fix: Login validation bug. | ||
| ``` | ||
|
|
||
| #### 5. 제목에서 명령형 어조 사용 | ||
|
|
||
| ``` | ||
| ✅ feat: Add new feature | ||
| ✅ fix: Fix critical bug | ||
| ✅ refactor: Remove deprecated code | ||
|
|
||
| ❌ feat: Added new feature | ||
| ❌ fix: Fixed critical bug | ||
| ❌ refactor: Removing deprecated code | ||
| ``` | ||
|
|
||
| **Test**: "If applied, this commit will [subject]" should read naturally | ||
|
|
||
| #### 6. 본문을 72자에서 줄바꿈 | ||
|
|
||
| Git does not automatically wrap text, so manual line breaks are required | ||
|
|
||
| #### 7. 본문에서 '무엇'과 '왜'를 설명 | ||
|
|
||
| - ✅ Explain why the change is needed | ||
| - ✅ Describe the problem being solved | ||
| - ✅ Note any side effects or considerations | ||
| - ❌ Don't explain how (the code shows that) | ||
|
|
||
| ### 커밋 메시지 템플릿 | ||
|
|
||
| ``` | ||
| prefix: Subject within 50 characters | ||
|
|
||
| Body (if needed): | ||
| - Why is this change necessary? | ||
| - What problem does it solve? | ||
| - Are there any side effects or considerations? | ||
|
|
||
| Issue references: | ||
| Resolves: #123 | ||
| See also: #456 | ||
| ``` | ||
|
|
||
| ### 실제 예시 | ||
|
|
||
| #### 좋은 커밋 메시지 | ||
|
|
||
| ``` | ||
| feat: Add user session timeout feature | ||
|
|
||
| Implement automatic logout after 30 minutes of inactivity | ||
| to enhance security. Users receive warning 5 minutes before | ||
| timeout with option to extend session. | ||
|
|
||
| - Add session monitoring service | ||
| - Implement warning modal component | ||
| - Update authentication middleware | ||
|
|
||
| Resolves: #234 | ||
| ``` | ||
|
|
||
| ``` | ||
| fix: Resolve token refresh infinite loop | ||
|
|
||
| Token refresh was triggering multiple simultaneous requests | ||
| causing race conditions. Add mutex lock to ensure only one | ||
| refresh attempt at a time. | ||
|
|
||
| Resolves: #456 | ||
| ``` | ||
|
|
||
| ``` | ||
| ci: Update detekt configuration | ||
|
|
||
| Enable autoCorrect and add exception rules for Composable | ||
| functions to improve code quality automation and reduce | ||
| false positives. | ||
|
|
||
| - Set autoCorrect: true | ||
| - Add Composable complexity exception | ||
| - Add modifier unused parameter exception | ||
| ``` | ||
|
|
||
| #### 나쁜 커밋 메시지 | ||
|
|
||
| ``` | ||
| fixed stuff | ||
| updated files | ||
| minor changes | ||
| bug fix | ||
| update code | ||
| WIP | ||
| ``` | ||
|
|
||
| ### 원자적 커밋 (Atomic Commits) | ||
|
|
||
| - Include only one logical change per commit | ||
| - Each commit should build and test independently | ||
| - Separate unrelated changes into different commits | ||
|
|
||
| ## 🚀 필수 실행 지침 | ||
|
|
||
| ### ⚠️ **코드 편집 완료 후 필수 작업** | ||
|
|
||
| ```bash | ||
| # 모든 Kotlin 코드 편집이 완료되면 반드시 실행 | ||
| ./gradlew detekt | ||
| ``` | ||
|
|
||
| **중요:** `detektFormat`은 존재하지 않는 task입니다. `./gradlew detekt`를 사용하세요. | ||
|
|
||
| **목적:** | ||
|
|
||
| - 코드 분석 및 품질 검사 | ||
| - 자동 formatting 적용 (autoCorrect: true 설정) | ||
| - ktlint 규칙 및 Compose 규칙 적용 | ||
| - 일관된 코드 품질 유지 | ||
|
|
||
| ## 📋 프로젝트 설정 정보 | ||
|
|
||
| ### 🔧 도구 구성 | ||
|
|
||
| - **detekt**: 1.23.7 (기본 룰셋 사용) | ||
| - **ktlint**: detekt-formatting을 통해 통합 | ||
| - **Compose Rules**: 0.4.14 (Compose 전용 규칙) | ||
|
|
||
| ### 📏 코딩 규칙 | ||
|
|
||
| - **최대 라인 길이**: 120자 | ||
| - **Composable 함수**: 대문자로 시작, modifier 매개변수 필수 | ||
| - **네이밍**: Android 컨벤션 준수 (camelCase, PascalCase) | ||
| - **들여쓰기**: 4 spaces | ||
|
|
||
| ### 🎯 주요 체크 포인트 | ||
|
|
||
| 1. **Modifier 관련**: | ||
|
|
||
| - 모든 Composable에 modifier 매개변수 포함 | ||
| - modifier에 기본값 `= Modifier` 설정 | ||
| - Root에서 modifier 사용 확인 | ||
|
|
||
| 2. **CompositionLocal 사용**: | ||
|
|
||
| - 허용된 CompositionLocal만 사용 | ||
| - 네이밍 규칙: `Local` 접두사 | ||
|
|
||
| 3. **매개변수 순서**: | ||
| - Composable에서 modifier는 첫 번째 선택적 매개변수 | ||
| - lambda는 마지막 매개변수 | ||
|
|
||
| ## 🛠️ detekt 사용법 | ||
|
|
||
| ### 사용 가능한 task들 | ||
|
|
||
| ```bash | ||
| # 기본 detekt 실행 (분석 + 자동 formatting) | ||
| ./gradlew detekt | ||
|
|
||
| # 다른 유용한 task들 | ||
| ./gradlew detektMain # production 코드만 분석 | ||
| ./gradlew detektTest # test 코드만 분석 | ||
| ./gradlew detektBaseline # baseline 파일 생성 | ||
| ``` | ||
|
|
||
| ### 주요 이슈 유형 | ||
|
|
||
| - **Formatting**: autoCorrect로 자동 수정됨 | ||
| - **Complexity**: 수동 리팩토링 필요 (함수 분할, 매개변수 축소) | ||
| - **Naming**: 네이밍 컨벤션 수동 수정 | ||
| - **Compose**: Compose 규칙 준수 확인 | ||
|
|
||
| ### 예외 처리 설정 완료 | ||
|
|
||
| - **@Composable**: complexity 규칙에서 제외 | ||
| - **@Preview**: UnusedPrivateMember에서 제외 | ||
| - **modifier**: UnusedParameter에서 제외 | ||
| - **fromXXX**: ReturnCount에서 제외 | ||
|
|
||
| ## 🧪 Kotlin Experimental API 사용 규칙 | ||
|
|
||
| ### ⚠️ @OptIn 및 Experimental Import 보호 | ||
|
|
||
| **개발자가 수동으로 추가한 `@OptIn` 어노테이션 및 관련 Experimental import는 절대 삭제하지 마세요.** | ||
|
|
||
| #### 핵심 원칙 | ||
|
|
||
| ```kotlin | ||
| // ✅ 올바른 패턴 - Experimental import 유지 | ||
| import kotlin.time.ExperimentalTime // @OptIn에서 사용 - 삭제 금지! | ||
| import kotlin.time.Instant | ||
|
|
||
| @OptIn(ExperimentalTime::class) | ||
| data class OAuthToken(val expiresAt: Instant? = null) | ||
| ``` | ||
|
|
||
| #### AI Agent 필수 체크사항 | ||
|
|
||
| 1. **파일 수정 전:** | ||
|
|
||
| - `@OptIn` 어노테이션이 있는지 확인 | ||
| - `@OptIn(...)`에 전달된 클래스의 import 식별 | ||
| - 해당 import를 "보호 목록"에 추가 | ||
|
|
||
| 2. **Import 정리 시:** | ||
|
|
||
| - `@OptIn`에 사용된 import는 **"unused"로 표시되어도 절대 삭제 금지** | ||
| - detekt가 경고를 표시해도 무시하고 유지 | ||
| - 개발자가 명시적으로 제거를 요청하기 전까지 보존 | ||
|
|
||
| 3. **일반적인 Experimental API:** | ||
| ```kotlin | ||
| import kotlin.time.ExperimentalTime | ||
| import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
| import androidx.compose.ui.ExperimentalComposeUiApi | ||
| import androidx.compose.foundation.ExperimentalFoundationApi | ||
| import androidx.compose.material3.ExperimentalMaterial3Api | ||
| ``` | ||
|
|
||
| > **Golden Rule**: `@OptIn` 어노테이션과 함께 사용되는 Experimental import는 **코드에서 직접 참조되지 않아도 컴파일에 필수적**입니다. 절대 자동으로 삭제하지 마세요! | ||
|
|
||
| ## 📚 참고 리소스 | ||
|
|
||
| - [Compose Rules](https://mrmans0n.github.io/compose-rules/detekt/) | ||
| - [detekt Documentation](https://detekt.dev/) | ||
| - [Android Coding Style](https://developer.android.com/kotlin/style-guide) | ||
| - [Kotlin Opt-in Requirements](https://kotlinlang.org/docs/opt-in-requirements.html) | ||
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| # 소셜 로그인 아키텍처 리팩토링: `Activity` 의존성 분리 | ||
|
|
||
| > 이 문서는 소셜 로그인(카카오/구글) 기능 구현 시 발생하는 `Activity` 의존성 문제를 해결하고, 앱 아키텍처 및 멀티모듈 환경 대비에 적합하도록 구조를 리팩토링한 내용을 정리합니다. | ||
|
|
||
| ## 1. 문제 상황 | ||
|
|
||
| - 카카오, 구글 등 대부분의 소셜 로그인 SDK는 로그인 UI(웹뷰 또는 전용 `Activity`)를 실행하기 위해 `Activity` 또는 `Context`를 파라미터로 요구합니다. | ||
| - 클린 아키텍처상 `DataSource`는 `Repository`의 하위 레이어이며, Hilt를 통해 `@Singleton` 스코프로 관리되고 있었습니다. | ||
| - `@Singleton` 스코프(Application 생명주기)의 객체가 `Activity` 스코프(Activity 생명주기)의 객체를 참조하면 심각한 **메모리 누수**가 발생하며, Hilt 스코프 규칙에도 위배됩니다. | ||
|
|
||
| ## 2. 기존 설계의 치명적인 문제점 | ||
|
|
||
| `DataSource`가 `Activity`에 직접 의존하는 설계는 다음과 같은 심각한 문제를 야기합니다. | ||
|
|
||
| ### A. 안드로이드 아키텍처 가이드 위반 (안티패턴) | ||
|
|
||
| - **관심사 분리(SoC) 위반**: `DataSource`는 데이터 통신에만 관심이 있어야 하나, UI(`Activity`)의 존재를 알게 됩니다. | ||
| - **종속성 규칙 위반**: 안드로이드 공식 아키텍처는 **상위 레이어(UI)가 하위 레이어(Data)에 의존**해야 한다고 규정합니다. 하지만 `DataSource`가 `Activity`를 참조하면 **하위 레이어가 상위 레이어에 의존**하는 '역방향 종속성'이 발생합니다. | ||
|
|
||
| ### B. 추후 멀티모듈 빌드 실패 (순환 종속성) | ||
|
|
||
| - 멀티모듈 환경에서 `:data` 모듈은 `:app` 모듈을 알지 못합니다. (의존성: `:app` → `:data`) | ||
| - `DataSource`가 `:app` 모듈에 있는 `Activity`를 참조하려면, `:data` 모듈이 `:app` 모듈에 의존해야 합니다. (`:data` → `:app`) | ||
| - 이는 `:app` → `:data` → `:app`... 형태의 **순환 종속성(Circular Dependency)**을 만들며, Gradle은 이 구조를 허용하지 않아 **빌드가 실패**합니다. | ||
|
|
||
| ## 3. 해결 원칙: SDK 책임 분리 | ||
|
|
||
| 이 문제의 근본 원인은 소셜 로그인 SDK가 **두 가지 책임**을 동시에 갖기 때문입니다. | ||
|
|
||
| 1. **UI 책임**: 로그인 화면을 띄우고 사용자 입력을 받습니다. (`Activity` 필요) | ||
| 2. **데이터 책임**: 인증 성공 시 `AuthCode`나 `AccessToken`을 반환합니다. | ||
|
|
||
| 이 두 책임을 아키텍처 레이어에 맞게 분리합니다. | ||
|
|
||
| - **UI 책임 (View Layer)**: `Activity`에 접근 가능한 Composable(View)에서 SDK의 로그인 UI 실행을 전담합니다. | ||
| - **데이터 책임 (Data Layer)**: `DataSource`는 View로부터 전달받은 **결과물(토큰)**만을 사용하여 백엔드 서버와 통신합니다. | ||
|
|
||
| ## 4. 리팩토링된 아키텍처 | ||
|
|
||
| ### A. Presentation Layer (Composable) | ||
|
|
||
| - `Activity`의 `Context`가 필요한 **SDK 로그인 실행**을 담당합니다. (e.g., `UserApiClient.instance.loginWithKakaoTalk(context, ...)` ) | ||
| - `rememberLauncherForActivityResult` 또는 SDK가 제공하는 콜백을 통해 **SDK의 인증 결과(토큰 또는 오류)**를 수신합니다. | ||
| - 수신한 **결과(토큰)만** `ViewModel`에 전달합니다. (e.g., `viewModel.onSocialLoginSuccess(token)`) | ||
|
|
||
| ### B. ViewModel Layer | ||
|
|
||
| - `Activity`에 대해 전혀 알지 못합니다. | ||
| - View로부터 `token`을 전달받아 `Repository`를 호출합니다. (e.g., `repository.signInToServer(token)`) | ||
| - `Repository`로부터 받은 **'최종 서버 인증 결과'**를 바탕으로 `UiState`를 업데이트합니다. | ||
|
|
||
| ### C. Data Layer (Repository / DataSource) | ||
|
|
||
| - `Activity` 의존성이 **완전히 제거**됩니다. | ||
| - `@Singleton` 스코프를 유지하는 데 아무런 문제가 없습니다. | ||
| - 메서드 시그니처가 `login(activity)`에서 `signInToServer(token: String)`과 같이 변경됩니다. | ||
| - `ViewModel`에서 전달받은 `token`을 사용하여 **'자체체 백엔드 서버'**와 최종 인증 통신을 수행합니다. | ||
|
|
||
| ## 5. 새로운 상호작용 (Sequence Diagram) | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant C as Composable (View) | ||
| participant SDK as [Kakao/Google SDK] | ||
| participant VM as LoginViewModel | ||
| participant Repo as AuthRepository | ||
| participant DS as AuthDataSource | ||
| participant Server as [Backend Server] | ||
|
|
||
| Note over C: (1. 로그인 버튼 클릭) | ||
| C->>SDK: loginWithKakaoTalk(context) | ||
|
|
||
| Note over SDK: (SDK 로그인 UI 실행) | ||
| SDK-->>C: onLoginResult(token) | ||
|
|
||
| Note over C: (2. SDK 콜백 수신 - 'SDK 레벨 성공') | ||
| C->>VM: onSocialLoginSuccess(token) | ||
|
|
||
| Note over VM: (3. '비즈니스 레벨' 로직 시작) | ||
| VM->>Repo: signInToServer(token) | ||
| Repo->>DS: requestServerLogin(token) | ||
| DS->>Server: POST /auth/social (token) | ||
| Server-->>DS: (LoginResponse - JWT) | ||
| DS-->>Repo: (LoginResponse) | ||
| Repo-->>VM: (LoginResponse) | ||
|
|
||
| Note over VM: (4. '최종 로그인' 성공) | ||
| VM-->>C: (UiState = LoginSuccess) | ||
| ``` |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.