diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt index 79982aac..b23ec298 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt @@ -3,6 +3,7 @@ package bums.lunatic.launcher.apps import android.app.Dialog import android.app.SearchManager import android.content.Intent +import android.content.pm.ApplicationInfo import android.graphics.Color import android.net.Uri import android.os.Bundle @@ -28,6 +29,7 @@ import bums.lunatic.launcher.model.AppInfo import bums.lunatic.launcher.model.SimpleContact import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.CategoryGrouper +import bums.lunatic.launcher.utils.CategoryManualMapper import bums.lunatic.launcher.utils.EnToKo import bums.lunatic.launcher.utils.JamoUtils import bums.lunatic.launcher.utils.SimpleTransliterater @@ -97,18 +99,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() { return binding.root } - private val categoryMap = mapOf( - "전체" to "ALL", - "게임" to "GAME", - "생산성" to "PRODUCTIVITY", - "소셜" to "SOCIAL", - "오디오" to "AUDIO", - "비디오" to "VIDEO", - "이미지" to "IMAGE", - "지도" to "MAPS", - "뉴스" to "NEWS", - "기타" to "UNDEFINED" // 혹은 UNKNOWN - ) + private var currentScope: String = CategoryGrouper.SCOPE_ALL override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -127,7 +118,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() { private var currentCategoryKey: String = "ALL" private fun setupCategorySpinner() { // 1. 스피너에 들어갈 데이터(표시 이름들) 준비 - val displayList = categoryMap.keys.toList() + val displayList = CategoryManualMapper.CATEGORY_MAP.keys.toList() // 2. 어댑터 설정 (기본 안드로이드 레이아웃 사용) val adapter = object : ArrayAdapter( @@ -154,7 +145,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() { binding.categorySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { val selectedDisplayName = displayList[position] - currentCategoryKey = categoryMap[selectedDisplayName] ?: "ALL" + currentCategoryKey = CategoryManualMapper.CATEGORY_MAP[selectedDisplayName] ?: "ALL" // 선택 변경 시 목록 갱신 (검색어 유지) fetchApps(binding.searchInput.text.toString()) @@ -421,6 +412,8 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() { } + + /** * 앱 목록을 불러오는 함수 * - 검색어(keyword)가 있으면 필터링을 수행합니다. @@ -467,7 +460,15 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() { .sort("lastUseDate", Sort.DESCENDING) .find() - val scopedAppsList = baseAppQuery.map { realm.copyFromRealm(it) } + val scopedAppsList = baseAppQuery.map { + val appCopy = realm.copyFromRealm(it) + + // 💡 수동 매퍼를 사용하여 카테고리 강제 재할당 + // it.systemCategoryInt는 AppInfo 모델에 시스템 카테고리 값이 저장되어 있다고 가정함 + val fixedCategory = CategoryManualMapper.getFixedCategory(it.pkgName ?:"", it.appName, it.systemCategoryInt) + appCopy.category = fixedCategory + appCopy + } .filter { appInfo -> val isLaunchable = try { pm.getLaunchIntentForPackage(appInfo.pkgName ?: "") != null @@ -479,7 +480,6 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() { val isCategoryMatch = if (currentCategoryKey == "ALL") { true } else { - // AppInfoGetter에서 저장한 값("GAME", "AUDIO" 등)과 비교 appInfo.category == currentCategoryKey } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/model/AppInfo.kt b/app/src/main/kotlin/bums/lunatic/launcher/model/AppInfo.kt index b76e024a..d59ea0c2 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/model/AppInfo.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/model/AppInfo.kt @@ -16,6 +16,7 @@ class AppInfo : RealmObject { var clickCount : Int = 0 var lastUseDate : Long = 0L var category : String? = null + var systemCategoryInt : Int = 0 var currentInstalled : Boolean = false var isInstalled : Boolean = false // [신규] 0: 항상 보이기(기본), 1: 검색 시만 보이기 (숨김) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/utils/CategoryManualMapper.kt b/app/src/main/kotlin/bums/lunatic/launcher/utils/CategoryManualMapper.kt new file mode 100644 index 00000000..400fd8ea --- /dev/null +++ b/app/src/main/kotlin/bums/lunatic/launcher/utils/CategoryManualMapper.kt @@ -0,0 +1,166 @@ +package bums.lunatic.launcher.utils + +import android.content.pm.ApplicationInfo +import java.util.Locale + +/** + * 로그 분석 및 키워드 기반 카테고리 자동 분류 유틸리티 + */ +object CategoryManualMapper { + val CATEGORY_MAP = mapOf( + "전체" to "ALL", + "게임" to "GAME", + "생산성" to "PRODUCTIVITY", + "소셜" to "SOCIAL", + "오디오" to "MUSIC", + "쇼핑" to "SHOPPING", + "금융" to "FINANCE", + "비디오" to "VIDEO", + "이미지" to "IMAGE", + "지도" to "MAPS", + "뉴스" to "NEWS", + "도구" to "TOOLS", + "기타" to "UNDEFINED" + ) + + // 1. 고정 패키지명 매핑 (가장 높은 우선순위) + private val manualMapping = mapOf( + "com.sec.android.app.myfiles" to "TOOLS", // 삼성 내 파일 + "com.google.android.apps.nbu.files" to "TOOLS", // 구글 Files + // VIDEO 카테고리 + "com.netflix.mediaclient" to "VIDEO", + "net.cj.cjhv.gs.tving" to "VIDEO", + "com.google.android.youtube" to "VIDEO", + "com.disney.disneyplus" to "VIDEO", + "com.coupang.mobile.play" to "VIDEO", + "kr.co.captv.pooqV2" to "VIDEO", // wavve + "com.synology.dsvideo" to "VIDEO", + "org.videolan.vlc" to "VIDEO", + "com.mxtech.videoplayer.ad" to "VIDEO", + "com.gretech.gomplayerko" to "VIDEO", + "com.google.android.videos" to "VIDEO", // Google TV + + // MAPS / TRAVEL 카테고리 + "com.nhn.android.nmap" to "MAPS", + "net.daum.android.map" to "MAPS", + "com.google.android.apps.maps" to "MAPS", + "com.skt.tmap.ku" to "MAPS", + "com.locnall.KimGiSa" to "MAPS", // 카카오내비 + "com.agoda.mobile.consumer" to "MAPS", + "com.airbnb.android" to "MAPS", + "com.mrt.ducati" to "MAPS", // 마이리얼트립 + + // FINANCE 카테고리 + "viva.republica.toss" to "FINANCE", + "com.kakaobank.channel" to "FINANCE", + "com.btckorea.bithumb" to "FINANCE", + "com.dunamu.exchange" to "FINANCE", // 업비트 + "com.truefriend.ministock" to "FINANCE", + "com.truefriend.neosmartarenewal" to "FINANCE", + "com.hyundaicard.appcard" to "FINANCE", + "kr.co.samsungcard.mpocket" to "FINANCE", + "com.kbcard.cxh.appcard" to "FINANCE", + "com.kbstar.kbbank" to "FINANCE", + "nh.smart.banking" to "FINANCE", + "com.kbankwith.smartbank" to "FINANCE", + + // SHOPPING 카테고리 + "com.coupang.mobile" to "SHOPPING", + "com.coupang.mobile.eats" to "SHOPPING", + "com.ebay.kr.gmarket" to "SHOPPING", + "com.ebay.kr.auction" to "SHOPPING", + "com.elevenst" to "SHOPPING", + "net.giosis.shopping.sg" to "SHOPPING", // Qoo10 + "com.alibaba.aliexpresshd" to "SHOPPING", + "com.einnovation.temu" to "SHOPPING", + + // GAME 카테고리 + "com.sundaytoz.mobile.anisachun.google.service" to "GAME", + "com.gof.global" to "GAME", // 화이트아웃서바이벌 + "com.tap4fun.odin.kingdomguard" to "GAME", + "com.dreamgames.royalmatch" to "GAME", + "com.ragequitgames.tomorrow" to "GAME" + ) + + // 2. 키워드 기반 카테고리 매핑 (두 번째 우선순위) + private val keywordMapping = mapOf( + "VIDEO" to listOf( + "video", "player", "movie", "tv", "cinema", "streaming", "media", "netflix", + "비디오", "플레이어", "영화", "티브이", "시네마", "스트리밍", "넷플릭스", "티빙", "유튜브" + ), + "GAME" to listOf( + "game", "vulkan", "unity", "unreal", "nexon", "netmarble", "lineage", + "게임", "넥슨", "넷마블", "카카오게임", "애니팡", "전투", "전략", "퍼즐", "RPG" + ), + "FINANCE" to listOf( + "bank", "card", "finance", "pay", "stock", "invest", "kb", "shinhan", "woori", "hana", + "은행", "카드", "금융", "페이", "증권", "주식", "투자", "뱅크", "뱅킹", "보험", "자산" + ), + "MUSIC" to listOf( + "music", "audio", "sound", "radio", "melody", "streaming", "melon", "bugs", + "뮤직", "음악", "오디오", "사운드", "라디오", "멜로디", "멜론", "벅스", "지니" + ), + "MAPS" to listOf( + "map", "navi", "taxi", "transport", "bus", "subway", "metro", "navigation", + "지도", "내비", "택시", "교통", "버스", "지하철", "네비", "길찾기" + ), + "IMAGE" to listOf( + "photo", "gallery", "image", "camera", "editor", "snap", "pic", + "사진", "갤러리", "이미지", "카메라", "편집", "스냅", "뷰어" + ), + "SOCIAL" to listOf( + "talk", "messenger", "chat", "social", "sns", "community", "kakao", "telegram", + "톡", "메신저", "채팅", "소셜", "커뮤니티", "카카오", "텔레그램", "밴드", "카페" + ), + "SHOPPING" to listOf( + "shop", "mall", "market", "delivery", "eats", "order", "commerce", "coupang", + "쇼핑", "몰", "마켓", "배달", "이츠", "주문", "커머스", "쿠팡", "마트", "백화점" + ), + "TOOLS" to listOf( + "file", "manager", "explorer", "cleaner", "storage", "calculator", "clock", "calendar", + "파일", "관리자", "탐색기", "클리너", "저장소", "계산기", "시계", "달력", "메모" + ), + ) + + /** + * 패키지명, 앱 이름, 시스템 카테고리를 종합하여 최적의 카테고리를 반환합니다. + */ + fun getFixedCategory(packageName: String?, appName: String?, systemCategory: Int): String { + // --------------------------------------------------------- + // [단계 1] 이전의 수동 맵(manualMapping) 확인 + // --------------------------------------------------------- + manualMapping[packageName]?.let { return it } + + // --------------------------------------------------------- + // [단계 2] 시스템 분류(systemCategory)가 명확한지 확인 + // --------------------------------------------------------- + val systemType = when (systemCategory) { + ApplicationInfo.CATEGORY_GAME -> "GAME" + ApplicationInfo.CATEGORY_VIDEO -> "VIDEO" + ApplicationInfo.CATEGORY_MAPS -> "MAPS" + ApplicationInfo.CATEGORY_AUDIO -> "MUSIC" + ApplicationInfo.CATEGORY_PRODUCTIVITY -> "PRODUCTIVITY" + ApplicationInfo.CATEGORY_SOCIAL -> "SOCIAL" + ApplicationInfo.CATEGORY_NEWS -> "NEWS" + ApplicationInfo.CATEGORY_IMAGE -> "IMAGE" + else -> null + } + if (systemType != null) return systemType + + // --------------------------------------------------------- + // [단계 3] 시스템 분류가 없는 경우 키워드 매핑 수행 + // --------------------------------------------------------- + val pkg = packageName?.lowercase(Locale.ROOT) ?: "" + val name = appName?.lowercase(Locale.ROOT)?.replace(" ", "") ?: "" + + for ((category, keywords) in keywordMapping) { + for (keyword in keywords) { + if (pkg.contains(keyword) || name.contains(keyword)) { + return category + } + } + } + + return "UNDEFINED" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/AppInfoGetter.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/AppInfoGetter.kt index a1a83280..e42ae1b1 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/AppInfoGetter.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/AppInfoGetter.kt @@ -47,6 +47,7 @@ class AppInfoGetter : BaseGetter { this.searchIndex = SimpleTransliterater.makeSearchIndex(appName) this.pkgName = pkgName this.category = getCategory(ri.activityInfo.applicationInfo.category) + this.systemCategoryInt = ri.activityInfo.applicationInfo.category this.alphaCho = AlphabetToChosungMap.getCho(appName) this.appNameChosung = JamoUtils.split(appName).joinToString("") })