본문으로 건너뛰기
5분 AI.
TRENDSHIFT DAILY #4 딥다이브 · 2026-10-23 분석

cleanyourtv 딥다이브
— TV 홈 화면에서 배우는 복구 가능한 자동화

기기 관리 도구

빠른 판단

무엇
ADB로 Android TV 패키지를 비활성화하고 단순한 홈 런처로 전환하며 상태·복구를 관리하는 도구 모음
이럴 때
보유 TV의 불필요한 앱과 홈 화면을 정리하고 변경 전 상태 기록을 바탕으로 되돌릴 수 있게 구성할 때
광고가 많은 TV 홈을 단순한 화면으로 바꾸는 프로젝트에서, 의외로 중요한 공부거리는 되돌리기와 실패 처리입니다. PC의 Python·셸 도구, TV의 Java 런처, 별도 앱으로 분리한 확장 기능을 실제 코드로 따라갑니다. 저장소 Gathin23/cleanyourtv · ⭐25,303 · 포크 2,788 · 주 언어 Java · 2026-10-23 GitHub 기준. 코드·설정·규모 수치는 커밋 c5d75e8 기준입니다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

TV를 새로 만드는 대신, 기존 Android의 제어 통로와 홈 앱을 조합한다

cleanyourtv는 Android TV·Google TV에 Wi-Fi로 연결해 불필요한 패키지를 비활성화하고, 작은 네이티브 런처를 홈 화면으로 쓰도록 돕는 도구 모음입니다. Apple TV를 닮은 화면을 제공하지만, TV 운영체제를 tvOS로 바꾸거나 새 펌웨어를 설치하는 프로젝트는 아닙니다.

이 저장소에서 배울 핵심은 기기를 바꾸는 명령보다, 바꾸기 전 상태를 보존하고 실패 뒤에 돌아오는 방법입니다. 예쁜 화면을 넘어 tools/recovery.py와 Home 전환 검사를 읽어야 프로젝트의 설계를 이해할 수 있습니다.

한 문장 비유

집을 허물지 않고, 가구 배치와 현관을 바꾸는 작업입니다

Android는 집의 골조이고, 제조사 앱은 가구와 설비이며, 런처는 매일 들어오는 현관입니다. 정리 도구는 안 쓰는 가구를 사용 중지하고, 런처는 새로운 현관을 제공합니다.

문제는 가구처럼 보이는 것이 전기 배선일 수 있다는 점입니다. HDMI 입력·리모컨·시스템 권한을 담당하는 앱까지 끄면 안 되므로, 목록 검사와 복구 기록이 필요합니다.

용어
런처와 ADB
런처는 홈 버튼을 눌렀을 때 뜨는 앱입니다. ADB(Android Debug Bridge)는 개발용으로 기기에 명령을 보내고 파일을 옮기는 통로입니다. 여기서는 PC가 ADB 클라이언트이고, TV가 명령을 받는 기기입니다.

1-1. PC와 TV가 맡는 일은 다르다

  1. PC에서 조사: tools/tv.py가 기기 정보·패키지·설정을 읽습니다.
  2. PC에서 변경 준비: tools/disable.sh가 목록을 검사하고 복구 기록을 저장합니다.
  3. TV에서 상태 변경: ADB로 전달된 pm disable-user --user 0 명령이 패키지를 비활성화합니다.
  4. TV에서 홈 실행: launcher/를 빌드한 Android 앱이 홈 화면을 표시합니다.

위 명령은 패키지 비활성화의 동작을 보여 주는 예시입니다. 런처의 Java 코드가 직접 디블로트 명령을 수행하는 구조도 아닙니다.

1-2. 비활성화는 삭제와 무엇이 다른가

pm disable-user --user 0는 사용자 0에서 해당 앱을 비활성화합니다. 앱을 제거하지 않으므로 다시 활성화할 수 있지만, 해당 앱이 제공하던 기능은 멈출 수 있습니다. hidden_apps는 더 약한 조작으로, 런처에서 타일만 숨깁니다. 앱 자체를 비활성화하지 않습니다.

구분해서 기억하기
타일 숨기기 → 앱 비활성화 → 앱 제거는 서로 다른 작업

같은 화면에서 아이콘이 사라져도 시스템에 미치는 영향은 다릅니다. Config.java의 hiddenApps와 disable.sh의 패키지 명령을 나란히 읽어 보세요.

1-3. AI는 어디에 있는가

PROMPT.md는 코딩 도우미가 조사·질문·백업·변경·확인을 순서대로 하도록 안내하는 절차 문서입니다. 런처는 AI 모델을 내장하지 않습니다. launcher/app/build.gradle에 직접 선언한 실행 의존성은 AndroidX 두 개이고, 런처의 AndroidManifest.xml에는 INTERNET 권한이 없습니다. 따라서 AI 활용의 학습 포인트는 추론 API 구현보다 도구를 사용하는 작업 절차의 명문화에 있습니다.

2왜 주목받는가

눈에 보이는 문제, 재현 가능한 절차, 모델별 지식의 결합

2-1. 단번에 이해되는 전후 변화

README는 2026-10-17 부모님 TV를 정리한 전후 모습을 게시했고, 해당 게시물이 740만 조회를 얻었다고 설명합니다. 4일 동안 170개 이상의 모델과 200명 이상의 기여자가 모였다는 내용도 README의 자체 설명입니다. 게시물 조회 수와 기여자 수의 출처는 README입니다.

선정 기준은 TrendShift Daily #4(2026-10-23)입니다. 2026-10-23 GitHub 기준 생성일은 2026-10-17, 스타는 25,303개입니다. 생성일에서 분석일까지 6일이라는 짧은 기간에 관심이 집중됐다는 사실은 확인되지만, 이 숫자가 안정성이나 모든 TV에서의 성공률을 뜻하지는 않습니다.

화제의 배경은 새 알고리즘보다, 불편한 TV 화면을 바꾸는 과정을 누구나 따라갈 절차로 묶었다는 데 있습니다. 이는 README의 전후 사례와 PROMPT.md·examples/ 구성을 바탕으로 한 해석입니다.

2-2. 대안과 비교하면 강점이 선명해진다

아래 표는 기본 Google TV 홈이라는 제품과, 런처만 교체하거나 ADB를 수동으로 쓰는 접근을 비교합니다. 다른 런처 제품 전체의 기능을 조사한 순위표는 아닙니다. 기본 홈의 광고·추천 문제는 이 저장소 README가 제시한 사용 동기입니다.

대안해결하는 범위cleanyourtv의 상대적 강점·대가
기본 Google TV 홈제조사·플랫폼이 제공하는 홈과 연동 기능별도 홈으로 교체해 화면 구성을 통제한다. 대신 Home·입력 전환의 기기별 호환성을 확인해야 한다.
런처 앱만 교체보이는 홈 화면을 바꾸는 작업화면뿐 아니라 패키지 목록·비활성화·복구 절차를 함께 제공한다. 작업 범위가 넓어 검토할 것도 많다.
수동 ADB 작업사용자가 명령과 복구 방법을 직접 구성목록 검사, Home 보호, 변경 전 상태 기록을 재사용한다. 그래도 목록의 적합성 판단은 필요하다.
cleanyourtv정리 도구 + 작은 홈 앱 + 모델별 자료절차와 소스를 함께 학습할 수 있다. SDK·펌웨어 정책에 따른 한계는 남는다.

2-3. 모델별 목록이 쌓이는 구조

커밋 c5d75e8의 examples/*/debloat.txt는 243개입니다. 목록마다 모델·Android 버전·펌웨어와 남겨 둔 기능을 기록하도록 AGENTS.md가 요구합니다. 같은 브랜드라도 부품과 펌웨어가 다를 수 있으므로, 이 자료는 만능 삭제 목록보다 검토할 출발점에 가깝습니다.

tests/test_examples.py는 예제와 README 표의 대응을 검사하고, tests/test_core_packages.py는 보호할 패키지 규칙이 여러 도구에서 어긋나지 않도록 검사합니다. 커뮤니티가 모은 지식을 코드와 테스트로 유지하는 방법도 배울 수 있습니다.

2-4. 소개 문구는 소스로 범위를 좁혀 읽자

3기술 스택 전체 지도

웹 서비스의 프론트·백엔드 대신, TV 화면·PC 제어·빌드 계층으로 읽는다

3-1. 프론트엔드: Java로 만든 Android 홈 화면

launcher/app/src/main/java/com/gathin/tvhome/가 핵심 UI입니다. React·웹뷰 기반이 아니라 Android의 Activity·View·XML 레이아웃을 사용합니다. MainActivity.java가 홈을 조립하고, AppAdapter.java가 앱 타일을 RecyclerView에 연결합니다.

계층실제 설정·버전확인한 파일
언어 수준Java 17, 소스·타깃 호환성 모두 17launcher/app/build.gradle
Android SDKcompileSdk 34, targetSdk 34, minSdk 26같은 파일의 android 블록
앱 목록 UIandroidx.recyclerview:recyclerview:1.3.2같은 파일의 dependencies
스프링 모션androidx.dynamicanimation:dynamicanimation:1.0.0같은 파일의 dependencies
JVM 단위 테스트junit:junit:4.13.2 — 테스트에만 사용testImplementation
앱 식별·버전기본 com.gathin.tvhome, versionCode 7, versionName '2.6'defaultConfig

2026-10-23 GitHub 기준 최신 릴리스 태그는 v2.6.0이고, 커밋 c5d75e8의 Gradle 내부 버전 문자열은 2.6입니다. 릴리스 태그와 앱 내부 버전명은 구별해서 읽어야 합니다. applicationId에는 launcherApplicationId Gradle 속성을 읽는 선택적 분기도 있습니다.

// launcher/app/build.gradle의 핵심 설정 발췌
android {
    compileSdk 34
    defaultConfig {
        minSdk 26
        targetSdk 34
        versionCode 7
        versionName '2.6'
    }
}
dependencies {
    implementation 'androidx.recyclerview:recyclerview:1.3.2'
    implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'
    testImplementation 'junit:junit:4.13.2'
}

3-2. 백엔드에 해당하는 부분: 원격 API 서버가 없는 제어 계층

코어 런처에는 홈 화면을 서비스하는 Node.js·Spring 서버나 중앙 데이터베이스가 없습니다. 앱 목록은 TV의 PackageManager, 입력 장치는 TvInputManager, 테마는 JSON과 앱 파일, 설정 선택은 SharedPreferences에서 얻습니다. 웹 서비스에서 “서버에 요청”하던 자리를 Android 시스템 서비스와 로컬 저장소가 채우는 셈입니다.

기능구현의존성·저장 위치
ADB 접속·파일 전송tools/tv.pyadb-shell==0.4.4, rsa==4.9.1, cryptography>=46.0.7
패키지 변경tools/disable.sh와 tools/disable.ps1Bash·PowerShell이 Python 클라이언트를 호출
복구 상태tools/recovery.py사용자 홈의 ~/.cleanyourtv/ 아래 JSON — 실제 도구를 실행할 때 생성
테마 읽기Config.load()XML 기본값·앱 외부 파일의 config.json·로컬 설정
카드 승인Cards.load()·Cards.decide()TV 앱 내부의 files/cards.json

Python 의존성의 출처는 tools/requirements.txt입니다. cryptography는 하한만 지정되어 있으므로 설치되는 정확한 버전은 이 파일만으로 확정할 수 없습니다. CI는 Python 3.12를 사용하지만 README의 사용자 요구사항은 Python 3으로 더 넓습니다.

3-3. 인프라·빌드: 배포 서버보다 APK 크기가 제약이다

항목확인값설계에 주는 영향
Android Gradle Plugin8.5.2 — launcher/build.gradleAndroid 앱 빌드 담당
Gradle Wrapper8.9 — launcher/gradle/wrapper/gradle-wrapper.properties배포 파일 SHA-256도 고정
CI 환경ubuntu-latest, Temurin JDK 17, Android 34, build-tools 34.0.0.github/workflows/ci.yml에 명시
APK 크기 제한491,520바이트 미만 — 480 KiB 미만경계값과 같아도 CI 실패; README의 약 474 KB와 구별
릴리스 최적화minifyEnabled true, shrinkResources trueR8과 리소스 축소로 앱 크기를 관리
선택적 성능 도구perfetto==0.58.2, pillow==11.3.0tools/requirements-perf.txt; 일반 정리에는 불필요

릴리스도 signingConfig signingConfigs.debug를 사용합니다. 직접 빌드해 ADB로 설치하는 배포 방식에 맞춘 선택이며, 앱스토어 배포용 서명 관리의 예제로 그대로 옮길 설정은 아닙니다. CI는 APK 게시보다 단위 테스트·Lint·빌드·크기 검증에 초점을 둡니다.

3-4. 확장 기능은 별도의 기술 스택을 가질 수 있다

addons/tv-hub/는 Kotlin 앱입니다. 루트 빌드 설정은 Android Gradle Plugin 8.7.3, Kotlin 플러그인 2.0.21이고, 앱 의존성에는 Media3 1.4.1, coroutines-android 1.8.1가 있습니다. 이것을 Java 런처의 의존성으로 잘못 합산하지 않아야 합니다.

addons/weather/app/src/main/AndroidManifest.xml에는 INTERNET 권한이 있고, Forecast.java는 Open-Meteo의 날씨·지오코딩·대기질 주소를 호출합니다. 작은 코어를 유지하는 방법은 모든 기능을 없애는 것이 아니라, 더 많은 권한이 필요한 기능을 별도 앱으로 분리하는 것입니다.

4아키텍처 심화 분석

전체 흐름과 네 가지 설계 패턴을 실제 함수로 연결한다

4-1. 시스템 구조도

[사용자 / 절차를 따르는 코딩 도우미] | v [PC: PROMPT.md에 따른 조사·질문·백업] | +-- examples/ 모델별 목록 +-- tools/disable.sh -----> tools/recovery.py | | 변경 전 상태를 먼저 저장 | v | ~/.cleanyourtv/*.json | +-- launcher/build-and-install.sh --> Gradle --> APK +-- tools/theme.sh --------------------------> 테마 파일 | v [tools/tv.py: adb_shell + RSA 인증] | | 로컬 네트워크 ADB/TCP, 기본 포트 5555 v [TV: Android 시스템] +-- 패키지 관리자: 비활성화·활성화·Home 지정 +-- com.gathin.tvhome: MainActivity | +-- PackageManager --> AppAdapter --> RecyclerView | +-- Config.load() --> XML / JSON / SharedPreferences | +-- ThemeProvider --> 허용한 테마 파일만 접근 | +-- Cards.discover() --> 카드 검증·승인 상태 조회 | +-- 별도 설치한 addons/ 앱 +-- 자기 화면·자기 권한·자기 의존성 +-- 카드 메타데이터를 통해 홈에 연결

코어 런처가 PC 도구를 실행하거나 서버에 목록을 요청하는 흐름은 없습니다. tv.py는 TV 호스트를 TV_HOST에서 받고, 포트는 주소 안의 포트·TV_PORT·기본 5555 순서로 정합니다. ADB 키를 이용한 인증과 통신 암호화는 다른 문제이며, README는 이 네트워크 ADB 경로가 평문이라고 설명합니다.

4-2. 패턴 ① 변경 전 상태 기록과 보상 작업

tools/disable.sh는 목록을 정규화하고 패키지 형식을 검사합니다. CORE_RE·CORE_OK로 핵심 패키지를 거부하고, Home 소유자·Wi-Fi 설정 소유자·TV 입력 관련 권한을 확인하는 보호 절차를 둡니다. 그 뒤 recovery.py --save가 성공해야 비활성화 명령 묶음을 전송합니다.

recovery.save()는 패키지별 enabled·disabled·absent 상태, 기존 Home 컴포넌트, 애니메이션 배율 설정을 기록합니다. 재시도 시 이미 기록한 패키지는 덮어쓰지 않고, 목록에 새로 추가된 이름만 보충합니다.

# tools/recovery.py의 write_record() 발췌
tmp = path.with_name(path.name + ".tmp")
with open(tmp, "w") as f:
    json.dump(record, f, indent=2, sort_keys=True)
    f.write("\n")
    f.flush()
    os.fsync(f.fileno())
os.replace(tmp, path)

임시 파일에 내용을 쓰고 동기화한 다음 최종 이름으로 교체합니다. 중간에 쓰기가 끊겨 복구 JSON이 반쪽이 되는 상황을 줄이려는 방식입니다. 원격 기기의 여러 변경을 한 번에 원자적으로 처리하는 데이터베이스 트랜잭션과는 다릅니다.

용어
변경 전 이미지와 보상 작업
작업 전 모습을 기록한 뒤, 실패하거나 취소하면 그 기록을 참고해 반대 작업을 하는 방식입니다. 여기서 복구는 “목록 전체를 켜기”가 아니라 “원래 켜져 있던 것을 다시 켜기”에 가깝습니다.

recovery.restore_commands()는 기록에서 enabled였던 패키지에 활성화 명령을 만들고, 일반 목록에서 원래 꺼져 있거나 없던 항목은 활성화 대상에서 제외합니다. 기존 Home과 배율도 가능한 경우 기록대로 되돌립니다. 다만 기본 런처는 기록상 disabled가 아니면 별도로 활성화를 시도합니다. 정리 전에 이미 이 프로젝트 런처가 Home이었다면 기본 런처로 돌아가는 별도 분기도 있습니다.

복구를 정확하게 만들려면 명령의 반대말보다, 사용자가 작업 전에 갖고 있던 상태를 알아야 합니다. 기록이 없을 때는 경고 후 전체 목록을 활성화하는 대체 동작이 있으므로, 정상 복구와 같은 의미로 취급하면 안 됩니다.

코드에서 확인한 한계

모든 확인 실패가 자동 중단으로 이어지지는 않습니다. disable.sh는 Home 소유자를 끝내 알 수 없거나 TV 입력 검사 연결이 실패하면 경고하는 경로도 있습니다. 보호 장치가 있다는 사실을 “어떤 목록을 넣어도 안전하다”로 확대하면 안 됩니다.

launcher/build-and-install.sh도 보상 작업을 사용합니다. SDK 사전 확인 → APK 설치 결과 확인 → am start -W로 새 홈 기동 확인 → 기존 홈 비활성화·Home 지정 → 재조회 순서입니다. Home이 예상 앱으로 해석되지 않으면 rollback_home()이 기존 런처를 다시 켜고 Home 복원을 시도합니다.

4-3. 패턴 ② 기본값 위에 설정을 겹치고, 잘못된 입력은 대체한다

Config.load()의 읽기 순서는 크게 리소스 기본값 → 테마 JSON → TV에서 고른 일부 설정입니다. SharedPreferences가 모든 JSON 키를 덮는 것은 아닙니다. 테마가 바뀌면 syncThemeChoices()가 일부 기존 선택을 초기화하는 로직도 있습니다.

// 학습용 최소 테마 예시: 실제 기본 테마 전체를 복사한 것은 아님
{
  "columns": 6,
  "rows_below_dock": 1,
  "reduced_motion": true,
  "background_color": "#101418",
  "wallpaper": false
}

integer()는 JSON 값이 숫자인지 확인한 뒤 intValue()로 바꾸고 범위를 검사합니다. columns 테마 범위는 1~8, TV 설정에서 읽는 선택 범위는 4~7입니다. 범위를 벗어난 숫자를 최댓값으로 잘라 넣지 않고 기존값으로 되돌립니다. 단, 이 함수는 소수도 숫자로 받아 정수 부분으로 바꾸므로 엄격한 정수 스키마와는 차이가 있습니다.

Config.read()는 64 × 1024바이트보다 큰 파일을 무시하고, BOM을 제거하며, 파싱 예외를 잡습니다. readOverlay()는 외부 저장소가 아직 마운트되지 않았으면 내부에 보관한 복사본을 읽습니다. 부팅 직후 저장소 준비가 늦어서 테마가 사라져 보이는 문제를 고려한 구현입니다.

비유

기본 설계도 위에 투명 필름으로 색과 배치를 얹는 방식입니다. 필름이 찢어졌다고 집 전체를 못 쓰게 만들지 않고, 기본 설계도로 돌아가도록 합니다.

쓰기 경로도 제한합니다. ThemeProvider.openFile()는 ThemeFiles.path()가 허용한 경로만 다룹니다. config.json, 지정된 배경 파일, 제한된 이름의 wallpapers/ 이미지 등이 대상이며, Manifest는 호출자에게 WRITE_SECURE_SETTINGS를 요구합니다. 파일 이름 제한과 호출 권한을 함께 쓰는 경계를 볼 수 있습니다.

4-4. 패턴 ③ 선언적 확장 계약과 장애 격리

모듈 카드는 런처 안에 확장 앱의 실행 코드를 불러오는 플러그인이 아닙니다. Cards.discover()가 패키지 관리자로 선언을 찾고, Cards.parse()가 XML 메타데이터를 읽어 타일의 이름·설명·강조색을 정합니다. 기본 실행 경로는 선언한 앱 자신의 Activity를 명시적 컴포넌트로 여는 방식입니다.

<!-- 학습 예시: 모듈 앱의 카드 XML 선언 -->
<card spec="1" label="Retro" caption="NES games" accent="#E8A33D" />

docs/specs/card.md와 소스의 SPEC_SUPPORTED = 1이 계약 버전을 맞춥니다. 라벨은 최대 24자, 설명은 최대 32자이며 허용 문자는 제한된 ASCII입니다. 따라서 이 버전의 카드 라벨에 한국어를 그대로 넣을 수 없습니다. 런처 전체의 번역 지원과 다른 제한입니다.

코드의 제한값막으려는 문제
MAX_DECLARED / MAX_SHOWN32개 / 8개한 번의 탐색량과 홈 타일 수 증가
MAX_EVENTS / MAX_DEPTH64 / 2지나치게 크거나 깊은 XML
MAX_ATTRIBUTES / MAX_RECORDS16 / 128선언 입력과 승인 기록의 무제한 증가

승인은 패키지명만으로 기억하지 않습니다. Cards.digest()가 서명 인증서의 SHA-256을 구하고, Cards.decide()가 패키지·서명·정규화한 라벨·결정을 저장합니다. 서명이 달라지면 RESIGNED, 정규화한 라벨이 달라지면 RELABELLED로 다시 승인을 기다립니다. 이 해시는 자동으로 신뢰성을 인증하는 도장이 아니라, 사용자가 승인한 대상을 계속 식별하는 값입니다.

// CardFeature.java의 동작을 줄여 쓴 의사 코드
if (broken) return fallback;
try {
    return step.run();
} catch (Exception | LinkageError e) {
    broken = true;
    return fallback;
}

CardFeature.run()은 실패 후 같은 프로세스에서 카드 기능을 건너뛰고 기본 타일을 사용하게 합니다. 선택 기능의 고장이 홈 전체로 번지지 않게 하는 회로 차단기와 비슷한 패턴입니다. CardFeatureTest.outOfMemoryIsNotSwallowed()는 모든 오류를 잡는 구현이 아님을 보여 줍니다.

4-5. 패턴 ④ 이벤트와 상태 기계로 불필요한 갱신을 줄인다

TileRefresh는 Android View가 없는 작은 상태 기계입니다. onStart()·onStop()·onPackagesChanged() 등의 입력을 받아, onResume()에서 NOTHING·REFOCUS·REBUILD 중 하나를 돌려줍니다.

상황선택효과
첫 화면 또는 앱 변경REBUILD앱 목록과 그림을 다시 구성
같은 앱 목록으로 홈 복귀REFOCUS목록 재생성 없이 포커스 복구
변화도 재포커스 요청도 없음NOTHING현재 구성을 유지

MainActivity.onResume()가 이 결과를 처리하고, reloadTiles()는 한 번의 LEANBACK_LAUNCHER 조회 결과를 타일과 카드 검사에 함께 전달합니다. onPause()에서는 배경·모션·분 단위 콜백·통계 갱신을 멈춥니다. 화면이 보이지 않을 때 불필요한 작업을 줄이는 생명주기 관리입니다.

restoreFocus()는 마지막 위치 숫자뿐 아니라 lastFocusedKey로 돌아갈 앱을 찾습니다. 앱이 추가되어 순서가 바뀌어도 같은 앱에 포커스를 돌려주려는 설계입니다. 기존 앱이 없어졌으면 이전 슬롯을 대안으로 사용합니다.

5디렉토리 구조 해부

화면을 바꾸는 코드와 기기를 바꾸는 코드를 먼저 구분한다
cleanyourtv/ ├── README.md 사용 방법·지원 범위·제약 ├── PROMPT.md 도우미가 따르는 TV 작업 절차 ├── AGENTS.md 기여·기기 작업 규칙과 저장소 안내 ├── LICENSE MIT 본문과 글꼴 라이선스 안내 ├── launcher/ 독립 Android Gradle 프로젝트 │ ├── build.gradle Android Gradle Plugin 버전 │ ├── build-and-install.sh 빌드·설치·Home 검증·복원 시도 │ └── app/ │ ├── build.gradle SDK·의존성·축소·앱 버전 │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/com/gathin/tvhome/ │ │ │ ├── MainActivity.java │ │ │ ├── AppAdapter.java │ │ │ ├── Config.java │ │ │ ├── Cards.java / CardFeature.java │ │ │ └── TileRefresh.java / GridNavigation.java │ │ └── res/ 레이아웃·번역·테마 기본값·그림 │ └── test/java/ JVM 단위 테스트와 테스트 보조 코드 ├── tools/ Python·Bash·PowerShell 도구 ├── tests/ 목록·문서·도구의 Python 테스트 ├── examples/ 모델별 debloat.txt와 적용 기록 ├── themes/ 재빌드 없이 읽는 config.json·배경 ├── addons/ 기능별 독립 Android 앱 ├── docs/ 카드 규격·기기 주의점·문제 해결 └── .github/workflows/ 코어 CI와 확장 앱 빌드 분리

5-1. 폴더마다 “무엇을 바꾸는가”가 다르다

폴더책임초보자가 먼저 볼 것
launcher/TV에서 실행되는 홈 앱GridNavigation.java처럼 작은 순수 로직부터
tools/PC에서 기기 조사·변경·복구·측정tv.py의 입력 처리와 recovery.py의 기록
examples/특정 모델·펌웨어에서 검토한 비활성화 목록첫 주석의 환경과 유지한 기능
themes/기존 설정 키로 표현하는 외형·배치themes/default/config.json
addons/런처에 합치지 않는 별도 기능 앱weather와 tv-hub의 Manifest 차이
docs/코드로 일반화하기 어려운 계약·환경 지식docs/specs/card.md와 docs/TROUBLESHOOTING.md

addons/에는 날씨·사진 화면보호기·수면 타이머·밝기 등 11개 앱 폴더가 있습니다. .github/workflows/addons.yml는 변경된 앱 폴더를 골라 따로 빌드합니다. 확장 앱의 의존성과 크기가 코어 런처 크기 제한에 섞이지 않는 구조입니다.

5-2. 코드 규모

집계 기준은 .git/를 제외한 파일 확장자별 물리적 줄 수입니다. 주석·빈 줄·테스트를 포함하며, 줄 끝 개행이 없는 마지막 줄도 한 줄로 계산합니다. 커밋 c5d75e8에 포함된 파일을 기준으로 한 소스 규모입니다.

언어·설정파일 수줄 수
Java18124,909
Kotlin 앱 코드 .kt343,664
Python9014,651
Bash .sh171,118
PowerShell8578
XML1866,958
Gradle Groovy / Kotlin 빌드 설정33 / 3590 / 88
YAML / JSON7 / 45259 / 459

Java 전체와 런처 구현량을 혼동하지 마세요. launcher/app/src/main/java/는 51개 파일·11,105줄이고, launcher/app/src/test/java/는 28개 파일·2,376줄입니다. Python 테스트 폴더에는 70개 파일·10,736줄이 있어, Python 전체 규모에서 테스트가 차지하는 비중이 큽니다.

MainActivity.java는 3,199줄, Config.java는 724줄, Cards.java는 864줄입니다. 큰 진입 파일을 처음부터 읽기보다, 작은 정책 함수와 테스트를 이해한 다음 호출 지점을 따라가면 구조가 보입니다.

5-3. 추천하는 소스 읽기 순서

  1. launcher/app/build.gradle과 Manifest로 실행 환경·권한을 파악합니다.
  2. GridNavigation.java·TileRefresh.java와 대응 테스트로 정책을 읽습니다.
  3. Config.load()에서 입력이 화면 설정이 되는 흐름을 확인합니다.
  4. MainActivity.onCreate()·onResume()·onPause()로 연결합니다.
  5. 마지막에 disable.sh·recovery.py와 설치 스크립트의 실패 경로를 읽습니다.

6학습 포인트 (기술별)

문법 하나가 제품의 어떤 문제를 푸는지 연결해 본다

6-1. Java: 화면에서 계산을 떼어 내는 방법

GridNavigation.atHorizontalEdge()는 현재 위치·앱 수·열 수·읽기 방향·누른 방향만 받습니다. Android 화면 없이도 결과를 계산할 수 있어 테스트하기 쉽습니다. GridNavigationTest는 마지막 행이 덜 찬 경우, 한 열인 경우, 오른쪽에서 왼쪽으로 읽는 경우를 다룹니다.

// GridNavigation.java의 실제 핵심 계산
boolean towardStart = left != rtl;
if (towardStart) return position % columns == 0;
return position % columns == columns - 1 || position == count - 1;

예를 들어 11개 앱을 4열로 놓으면 마지막 행은 위치 8·9·10입니다. 위치 10은 열 끝 번호 3이 아니어도 실제 마지막 앱이므로 바깥 경계입니다. 이 함수는 유효한 위치와 양수 열 수를 받는 내부 도우미이므로, 임의 입력 검증까지 담당한다고 읽으면 안 됩니다.

용어
순수 계산과 경계값
입력 숫자·논리값으로 답을 계산하고 외부 상태를 바꾸지 않는 부분은 따로 시험하기 좋습니다. 첫 칸·마지막 칸·덜 찬 행처럼 규칙이 달라지는 지점을 경계값이라고 부릅니다.

실습 아이디어: 위치 0·3·8·10의 좌우 경계 표를 종이에 작성하세요. 그다음 GridNavigationTest.horizontalEdgesMatchExplicitRowsAcrossColumnCountsAndDirections()의 별도 행 모델이 어떻게 기대값을 만드는지 읽어 보세요. 구현 공식을 그대로 반복하는 테스트와 다릅니다.

6-2. Android: 생명주기는 자원 사용 시간표다

MainActivity.onResume()에서는 배경 회전과 시계 콜백 등을 시작하고, onPause()에서는 멈춥니다. 앱을 나갔다 돌아올 때 목록이 바뀌었는지는 TileRefresh가 판단합니다. “화면을 다시 열었다”와 “데이터를 전부 다시 읽어야 한다”를 구분하는 연습입니다.

실습 아이디어: 홈 → 동영상 앱 → 홈 복귀와, 홈 → 다른 앱 설치 → 홈 복귀를 각각 추적하세요. REFOCUS와 REBUILD가 갈리는 입력을 표시하고, 기억하던 앱이 제거된 경우의 포커스 대안도 적어 보세요.

6-3. JSON·파일: 유연함에는 입력 경계가 필요하다

Config.bool()은 문자열 "true"를 Boolean true로 자동 변환하지 않습니다. Config.integer()는 숫자와 범위를 확인합니다. ThemeFiles.path()는 단순히 사용자가 준 경로를 이어 붙이지 않고 허용할 파일 구조를 고릅니다.

실습 아이디어: 같은 columns 키에 6·"6"·99·6.9를 넣었다고 가정하고, 기본값이 4일 때 결과를 추론하세요. 숫자 타입 검사와 정수만 허용하는 검사가 다르다는 점을 설명할 수 있으면 좋습니다.

6-4. Python·네트워크: 텍스트와 바이너리를 구분한다

tools/tv.py의 screenshot()은 스크린샷을 셸 문자열로 받아 저장하지 않습니다. TV 임시 경로에 PNG를 만든 뒤 dev.pull()로 가져오고, PNG 시그니처와 파일 크기를 확인합니다. 셸 출력의 텍스트 디코딩이 이미지 바이트를 망가뜨릴 수 있기 때문입니다.

tools/b64push.py는 다른 파일 전송 경로를 제공합니다. CHUNK = 48 * 1024를 기기의 최대 전송량에 맞춰 줄이고, 청크 크기를 3의 배수로 맞춰 base64 패딩이 중간에 끼는 문제를 피합니다. 임시 파일의 MD5가 맞을 때만 목적 파일로 옮깁니다. 여기서 MD5는 전송 오류 확인용이며 발행자 신뢰의 증거는 아닙니다.

실습 아이디어: 전체 파일을 3바이트씩 자르는 경우와 4바이트씩 자르는 경우의 base64 패딩 위치를 비교하세요. 이미지 데이터가 왜 일반 문자열과 다르게 다뤄져야 하는지도 한 문장으로 적어 보세요.

6-5. 테스트·CI: 실제 기기 없이도 검증할 정책이 많다

tests/test_recovery_record.py는 임시 저장소에 가짜 tv.py를 놓고 JSON으로 응답을 흉내 냅니다. 실제 복구 로직은 유지하면서 ADB 통신만 대체하는 구조입니다. “작업 전부터 꺼진 패키지를 복구가 켜 버리는가”처럼 반복하기 어려운 상황을 재현합니다.

CI의 Python 테스트, 런처 JVM 테스트, Android Lint, APK 크기 검사는 서로 다른 결함을 겨냥합니다. 테스트하기 쉬운 제품은 실제 기기가 없어도 판단할 정책을 작은 함수와 모의 통신 경계로 분리합니다. 다만 모의 테스트가 실제 리모컨·펌웨어·HDMI 동작을 보장하지는 않습니다.

6-6. 성능·모션: 느낌도 설정값으로 읽을 수 있다

Motion.java는 모션 감소 설정의 공통 기준을 담습니다. REDUCED_MS = 90, 스프링 강성 15000f, 최소 변화량 2f를 정하고, reduce()에서 튀지 않는 감쇠 비율을 적용합니다. 이것이 실제 모든 화면에서 90ms에 끝난다는 실측 결과를 뜻하지는 않습니다.

실습 아이디어: “애니메이션 끄기”와 “짧고 튀지 않는 이동으로 줄이기”가 포커스 인지에 주는 차이를 설명하세요. 낮은 사양을 위한 설정이 접근성 요구와도 만나는 지점을 찾을 수 있습니다.

7하드웨어 / 시스템 요구사항

소스 읽기, 앱 빌드, 실제 TV 작업의 요구사항을 구분한다
하려는 일필요 조건근거·제약
소스 공부텍스트 편집기와 로컬 저장소TV·AI 구독·GPU가 없어도 본문의 핵심 학습 가능
줄 수 집계·자료 검사Python 3 또는 읽기용 셸 도구텍스트 파일을 읽어 집계하는 데 사용
런처 빌드JDK 17, Android SDK 34, build-tools 34.0.0, Gradle Wrapper 8.9CI와 README 기준; 최초 의존성 다운로드는 별도 네트워크 작업
런처 실행SDK 26, 즉 Android 8.0 이상인 호환 TVminSdk 26; 펌웨어가 설치와 Home 전환을 허용해야 함
실제 원격 제어같은 로컬 네트워크의 PC와 TV, TV가 허용한 ADB 접근사용자의 디버깅 허용과 기기별 설정 필요
TV 메모리README는 1 GB TV를 목표로 명시측정한 최소 RAM 보장치나 성능 벤치마크는 아님

PC의 빌드 환경과 TV의 기기별 성능 요구는 구분해야 합니다. 작은 APK라고 실행 중 이미지·레이아웃 메모리까지 같은 크기인 것은 아닙니다. 또한 targetSdk 34는 앱의 대상 동작 기준이고, 최소 실행 버전을 뜻하지 않습니다.

7-1. TV라는 이름보다 운영체제와 정책이 중요하다

실제 기기 작업을 검토할 때

이 문서는 설치 실행 안내가 아니라 코드 학습 자료입니다. 실제 작업은 정확한 모델·펌웨어, 백업, 가족이 쓰는 기능, 원래 Home과 복구 경로를 먼저 확인해야 합니다. 저장소는 루팅·삭제·펌웨어 플래시 대신 비활성화를 원칙으로 합니다.

기본 포트 5555의 네트워크 ADB는 평문이며, 일부 펌웨어는 인증 없이 셸을 허용할 수 있습니다. README와 PROMPT.md는 작업 후 디버깅 해제·인증 철회·재부팅·포트 닫힘 확인을 요구합니다.

8직접 해볼 수 있는 실습 과제

TV 연결 없이 시작하는 네 과제 — 정답보다 판단 근거를 남긴다

아래 과제는 모두 소스 읽기·표 작성·모의 데이터 설계로 완수할 수 있습니다. 실습 메모와 별도로 만든 학습 코드는 원본 소스와 구분해 보관하세요. 다음 명령은 저장소 루트에서 파일을 읽기만 하며, 설치나 기기 연결을 하지 않습니다.

# 저장소 루트에서 참고 파일 읽기
sed -n '1,100p' launcher/app/build.gradle
cat themes/default/config.json
cat launcher/app/src/main/java/com/gathin/tvhome/GridNavigation.java

# 파일 목록과 한 파일의 물리적 줄 수 확인
find examples -name debloat.txt -type f
wc -l launcher/app/src/main/java/com/gathin/tvhome/MainActivity.java
과제 1 · 설정 추적

테마 한 키가 화면에 도착하는 길 입문

목표: columns와 reduced_motion을 골라 기본값·덮어쓰기·사용 지점을 찾습니다.

  1. launcher/app/src/main/res/values/config.xml에서 두 키의 기본값을 찾습니다.
  2. themes/default/config.json에 키가 있는지 확인합니다. 없는 키는 무엇을 따르는지 설명합니다.
  3. Config.load()에서 읽는 타입과, MainActivity.java에서 사용하는 필드를 연결합니다.

제출물: “파일·키·값·다음 호출 위치” 네 열의 추적 표. 완료 기준: JSON 파일 수정과 APK 재빌드의 차이, 잘못된 타입의 대체값을 설명할 수 있습니다.

과제 2 · 포커스 규칙

덜 찬 마지막 행의 좌우 이동 설계 중급

목표: 11개 앱·4열 화면을 그리고 LTR과 RTL의 물리적 좌우 경계를 계산합니다.

  1. 앱 위치 0~10을 행별로 적고, 각 행의 실제 첫 칸·끝 칸을 표시합니다.
  2. 위치 0·3·8·10에서 왼쪽·오른쪽을 눌렀을 때 경계인지 표로 작성합니다.
  3. GridNavigationTest의 기대값과 비교하고, 빈 도크 슬롯은 왜 DockSlots의 도움이 필요한지 읽습니다.

제출물: 최소 16개 입력 조합과 기대 결과. 완료 기준: 단순히 나머지 연산식을 복사하지 않고, 화면의 실제 행 길이로 정답을 설명합니다.

과제 3 · 복구 계획

아무 기기도 건드리지 않는 복구 계획기 중급

목표: 변경 전 상태에서 “다시 켤 항목”만 계산하는 규칙을 모의 JSON으로 설계합니다.

{
  "packages": {
    "com.example.ads": "enabled",
    "com.example.old": "disabled",
    "com.example.missing": "absent"
  }
}

학습 범위: 위 자료는 실제 복구 기록의 일부 필드만 흉내 냅니다. 결과는 com.example.ads 하나를 활성화 대상으로 고르는 것입니다. 원본 기록 전체를 해석하는 실행 도구로 사용하지 않습니다.

추가 조건: 두 번째 시도에서 ads가 이미 꺼져 있어도 최초 기록을 덮어쓰지 않는 규칙을 설계하세요. 완료 기준: 원래 꺼진 앱, 원래 없던 앱, 잘못된 상태 문자열, 새로 추가된 목록 항목을 각각 구분합니다.

과제 4 · 확장 계약

카드 승인 상태와 실패 경계 설계 고급

목표: 패키지·서명·라벨 중 무엇이 바뀌면 재승인을 요구하는지 상태 전이 표를 만듭니다.

  1. Cards.read()·decide()와 docs/specs/card.md의 승인 조건을 대조합니다.
  2. 같은 패키지·같은 서명 재설치, 서명 변경, 라벨 변경, 이름 충돌을 각각 입력 사례로 만듭니다.
  3. 한 카드 XML 오류와 CardFeature.run()의 기능 전체 오류를 구분합니다.
  4. CardFeatureTest에서 일반 예외·클래스 초기화 오류·메모리 부족을 다루는 차이를 기록합니다.

제출물: 8개 이상의 정상·오류 사례와 기대 상태. 완료 기준: “해시가 같으면 무조건 안전”이라고 설명하지 않고, 기존 승인의 유효 범위를 판단할 수 있습니다.

학습 결과 확인

코드를 따라 썼다는 것보다 “왜 이 입력에서는 기존 상태를 유지하는가”를 설명하는 것이 중요합니다. 각 과제에 정상 사례 하나와 실패 사례 하나의 근거 파일·함수를 붙여 보세요.

9관련 기술 심화 학습 로드맵

문법 → 로컬 앱 → 경계 → 복구 → 검증 순으로 확장한다

주당 3~5시간을 잡는 학습 계획 예시입니다. 소스 읽기는 TV 없이 진행하고, Android 빌드·기기 실험은 별도 환경을 준비한 뒤 선택할 수 있습니다. 먼저 아래 읽기 결과물을 완성하면 실제 기기가 없어도 설계를 이해했다는 근거가 남습니다.

주차학습 주제저장소에서 읽을 곳남길 결과물
1주Java 조건문·나머지 연산·클래스·열거형GridNavigation, TileRefresh포커스 경계 표와 3가지 갱신 동작 설명
2주Activity·Intent·앱 목록·RecyclerViewManifest, MainActivity.onCreate(), AppAdapter홈 버튼에서 타일 실행까지의 호출 흐름도
3주JSON 타입·파일 경로·설정 우선순위Config.load(), ThemeFiles.path()잘못된 입력 10개와 대체 동작 표
4주TCP·인증·텍스트와 바이너리·전송 단위tv.connect(), screenshot(), b64push.pyPNG 전송과 base64 전송의 차이 설명
5주변경 전 기록·원자적 파일 교체·보상 작업recovery.save(), restore_commands()최초 실행·재시도·복구의 상태 전이도
6주선언적 확장·서명 식별·테스트·CICards, CardFeatureTest, ci.yml모의 카드 검토표와 APK 크기 예산 설명

매주 기능을 더하기 전에 실패 사례 하나를 설명할 수 있으면, 이 저장소의 설계 의도를 더 정확하게 배울 수 있습니다. 3주에는 잘못된 테마, 4주에는 손상된 전송, 5주에는 반쯤 적용된 목록, 6주에는 서명이 바뀐 앱을 예로 들 수 있습니다.

9-1. 관심 분야에 따른 다음 갈림길

10핵심 키워드 사전

처음 읽을 때 막히는 단어를 이 저장소의 역할로 설명한다
용어쉬운 설명
디블로트불필요하다고 판단한 선탑재 앱·서비스를 정리하는 일. 여기서는 제거보다 사용자별 비활성화를 사용합니다.
런처홈 버튼을 눌렀을 때 앱과 입력 메뉴를 보여 주는 홈 앱입니다.
ADBPC가 Android 기기에 개발용 명령을 보내거나 파일을 옮기는 통로입니다.
패키지 / 컴포넌트앱의 식별 단위 / 앱 안의 특정 Activity 등의 식별 단위. Home 복원에는 구체적인 컴포넌트가 필요할 수 있습니다.
IntentAndroid에서 “홈을 열어라”, “설정을 보여 줘라” 같은 동작을 전달하는 메시지입니다.
Activity 생명주기화면의 생성·복귀·일시정지·정지에 맞춰 일을 시작하고 멈추는 규칙입니다.
PackageManager설치 앱과 실행 가능한 화면·메타데이터 등을 조회하는 Android 시스템 API입니다.
RecyclerView / Adapter앱 타일 같은 반복 목록을 표시하는 UI / 데이터를 그 목록 항목에 연결하는 부품입니다.
도크 / 포커스자주 쓰는 앱을 두는 영역 / 지금 리모컨으로 선택 중인 대상입니다.
RTL오른쪽에서 왼쪽으로 읽는 배치 방향. 같은 위치 번호라도 물리적 좌우 끝이 달라집니다.
SDK 26 / SDK 34Android 8.0 / Android 14에 대응하는 API 수준입니다. 최소·타깃·컴파일 설정의 역할을 구별해야 합니다.
APKAndroid 앱 설치 파일. 파일 크기와 실행 중 메모리 사용량은 다른 수치입니다.
Gradle / R8앱 빌드를 구성·실행하는 도구 / 사용하지 않는 코드를 줄이고 최적화하는 Android 빌드 도구입니다.
오버레이 설정기본값을 통째로 바꾸지 않고, JSON에 있는 키만 겹쳐 적용하는 방식입니다.
SharedPreferencesTV에서 사용자가 고른 설정 같은 작은 키·값을 앱 안에 저장하는 Android 기능입니다.
ContentProvider권한을 확인하면서 앱의 데이터·파일에 접근할 통로를 제공하는 Android 구성 요소입니다.
원자적 파일 교체완성한 임시 파일을 최종 경로로 교체해, 쓰다 만 본문이 정상 파일처럼 보이는 문제를 줄이는 방식입니다.
보상 작업여러 단계 작업 후 문제가 생겼을 때, 이전 상태로 돌아가기 위해 수행하는 복구 동작입니다.
상태 기계현재 상태와 입력 이벤트를 보고 다음 행동을 정하는 모델. 여기서는 목록 재생성·재포커스·유지를 고릅니다.
회로 차단기고장 난 기능을 계속 부르지 않게 잠시 막는 방식. 카드 오류 후 기본 홈을 유지하는 구현과 닮았습니다.
서명 인증서 다이제스트앱 서명 인증서를 해시로 요약한 값. 카드 승인을 특정 서명과 연결하는 식별자로 씁니다.
base64바이너리를 문자로 옮기는 표현 방식. 암호화가 아니며 일반적으로 크기가 늘어납니다.
모의 객체·가짜 통신실제 TV 대신 준비한 응답을 돌려주는 대역. 위험하거나 재현하기 어려운 상황을 검증할 때 씁니다.
얕은 클론일부 최근 이력만 가져온 저장소 복사본. 파일은 읽을 수 있지만 전체 개발 이력의 통계를 내기에는 부족합니다.
NOASSERTIONGitHub API의 라이선스 판정값. 그 값만으로 실제 LICENSE 파일의 조건을 대체할 수 없습니다.

11참고 링크

현재 수치의 출처, 커밋에 고정한 소스, 다음에 읽을 공식 문서

11-1. 현재 상태 (2026-10-23 확인)

스타·포크·생성일·최근 push·릴리스·API 라이선스는 2026-10-23 GitHub 기준입니다. 설정값·코드 규모·라이선스 파일은 커밋 c5d75e8 기준입니다.

항목확인값출처
저장소Gathin23/cleanyourtv2026-10-23 GitHub 기준
스타 / 포크25,303 / 2,7882026-10-23 GitHub API 기준
생성일 / 최근 push2026-10-17 / 2026-10-232026-10-23 GitHub API 기준
최신 릴리스v2.6.0 · 2026-10-222026-10-23 GitHub API 기준; 런처 내부 버전명은 2.6
최근 커밋c5d75e8e8071d6545e78ccbb6b4a303774f2e048
2026-10-23 · Merge pull request #1013 from maxhayim/official-links
커밋 c5d75e8 정보
주 언어Java2026-10-23 GitHub API 기준
코드 규모Java 24,909줄 · Python 14,651줄 · Kotlin 앱 코드 3,664줄커밋 c5d75e8; 주석·빈 줄·테스트 포함
런처 구현Java 51개 파일·11,105줄launcher/app/src/main/java/
모델 목록 / 테마debloat.txt 243개 / config.json 7개examples/*/ / themes/*/ 파일 수
라이선스API: NOASSERTION
루트 파일: MIT License, Copyright © 2026 Gathin
2026-10-23 GitHub API / 커밋 c5d75e8의 LICENSE
글꼴Outfit · SIL Open Font License 1.1루트 LICENSE와 launcher/FONT-LICENSE-OFL.txt
소스 기준커밋 c5d75e8의 설정·구현·물리적 줄 수기기별 설치·실행 결과와는 구별

11-2. 프로젝트와 분석한 소스

11-3. 관련 기술의 공식 학습 문서

쓰기 전에 알아둘 한계
정리 도구를 쓴다는 것은 시스템 기능의 의존관계까지 검토하는 일입니다

기기 호환성: 다른 모델의 목록을 그대로 적용하지 마세요. 홈·Wi-Fi·HDMI·튜너·키보드·리모컨 관련 패키지를 잘못 끄면 기능이나 접근 경로를 잃을 수 있습니다. 복구 스크립트도 ADB가 끊긴 기기를 자동으로 되살리는 수단은 아닙니다.

권한과 통신: 네트워크 권한이 없는 것은 코어 런처에 대한 사실입니다. 위치 정보·특수 설정 권한의 용도를 별도로 읽고, 날씨 등 확장 앱의 네트워크 권한도 각각 확인해야 합니다.

자료 범위: 코드 설명은 커밋 c5d75e8 기준이며, 기기별 성공률이나 전체 보안 감사 결과를 뜻하지 않습니다. 릴리스 정보와 코드 설명의 기준 시점은 구별해서 읽어야 합니다.

라이선스: API의 NOASSERTION과 루트 MIT 표기는 다릅니다. 재사용·재배포 시 실제 파일의 MIT 조건, 글꼴의 OFL, 확장 앱과 제3자 의존성의 별도 고지를 확인해야 합니다.