워치앱의 MainActivity.kt에서 Unresolved reference 'dayKey' 빌드 에러 발생
타일/메인 화면 일부 텍스트가 길어 줄임표(...)로 보이는 문제
스크롤바가 배경과 유사해 가시성이 떨어짐
하드코딩된 한글 문자열의 국제화(strings.xml) 필요
// 컴파일 에러 예시 (요약) // e: MainActivity.kt: Unresolved reference 'dayKey' // 자정 체크 로직에서 dayKey 상태가 정의되어 있지 않음
해결 과정 (How I Solved It)
dayKey 상태 추가: 자정 변경 감지를 위해 remember { mutableStateOf(...) }로 dayKey를 선언
스크롤바 색 개선: 배경색의 보색(Complementary color)로 스크롤바 트랙/썸을 그려 가시성 상승
텍스트 레이아웃 개선: 여백 조정 및 항목 분리로 줄임표 빈도 감소, 향후 Chip/버튼 폭/높이 조정 계획 반영
국제화: 화면에 보이는 주요 한글 문자열은 stringResource(...)로 대체하고 리소스로 이동(추가 정리 예정)
// 핵심 수정 예시: dayKey 상태 추가 및 자정 리셋 로직 유지 @Composable fun WearApp() { // ... 기존 상태들 ... // 일자 키(자정 교체 감지용) var dayKey by remember { mutableStateOf( java.time.ZonedDateTime.now() .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")) ) }
// 자정 변경 감지: 1분마다 체크하여 dayKey 달라지면 스텝/PDR 리셋 LaunchedEffect(Unit) { while (true) { val current = java.time.ZonedDateTime.now() .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")) if (current != dayKey) { dayKey = current // 스텝 리셋 및 PDR 리셋 passiveStepsManager.stop(); passiveStepsManager.start() pdr.reset() } kotlinx.coroutines.delay(60_000L) } } }
// 스크롤바 가시성 개선: 배경색 보색 사용 val animatedBg by animateColorAsState(targetValue = bgColor, animationSpec = tween(150)) val scrollbarColor = Color(1f - animatedBg.red, 1f - animatedBg.green, 1f - animatedBg.blue)
Canvas(modifier = Modifier .align(Alignment.CenterEnd) .padding(end = 2.dp) .height(100.dp) .width(3.dp)) { val contentHeight = scrollState.maxValue.toFloat() + size.height if (contentHeight > size.height + 1f) { val ratio = size.height / contentHeight val thumbHeight = (size.height * ratio).coerceAtLeast(12f) val scrollY = (scrollState.value / scrollState.maxValue.toFloat()).coerceIn(0f, 1f) val thumbTop = (size.height - thumbHeight) * scrollY drawRect(color = scrollbarColor.copy(alpha = 0.2f), size = size) // 트랙 drawRect(color = scrollbarColor, topLeft = Offset(0f, thumbTop), size = Size(size.width, thumbHeight)) // 썸 } }
// NaN 방어를 위해 앞값 채움(fill-forward)으로 시계열을 안전화한 부분 fun fillForward(arr: FloatArray): List<Float> { val out = ArrayList<Float>(arr.size) var last = 0f var seen = false for (i in arr.indices) { val v = arr[i] if (!v.isNaN()) { last = v; seen = true; out.add(v) } else { out.add(if (seen) last else 0f) } } return out }
해결 과정 (How I Solved It)
EMA(지수평활) — 간단·저비용 필터
정의: y[n] = α·x[n] + (1-α)·y[n-1]
특징: 구현 매우 쉬움, 레이턴시 낮음, 이상치에는 약함.
// 초보자도 이해하기 쉬운 EMA 필터 — 주석 상세 버전 class EmaFilter(private val alpha: Float) { private var state: Float? = null // 이전 출력값(초기엔 없음)
// 새 입력값 x를 받아 필터링된 값 반환 fun update(x: Float): Float { val s = state val out = if (s == null) { // 첫 샘플은 기준값으로 사용 x } else { // EMA 핵심 — 현재 입력과 이전 출력의 가중 평균 alpha * x + (1 - alpha) * s } state = out return out }
fun reset() { state = null } }
Kalman — 상태공간 기반 최적 추정(선형/가우시안 가정)
특징: 모델(A,H), 노이즈(Q,R)로 예측/갱신. 드리프트·노이즈 균형적으로 제어.
장점: 통계적으로 최적(가정 하), 속도/가속 등 상태 추정 가능.
단점: 튜닝 복잡, 계산량 EMA보다 큼.
// 1D 고도용 아주 간단한 Kalman — 주석 상세 class SimpleKalman1D( private var q: Float, // 프로세스(모델) 노이즈 공분산 — 모델 불확실성 private var r: Float // 측정 노이즈 공분산 — 센서 불확실성 ) { private var x: Float = 0f // 상태(고도) private var p: Float = 1f // 상태 오차 공분산
fun init(initial: Float, initialP: Float = 1f) { x = initial p = initialP }
// 예측 단계 — 간단 모델(고정)에서는 p만 늘려 불확실성 반영 fun predict() { p += q }
// 갱신 단계 — 관측 z 반영 fun update(z: Float): Float { val k = p / (p + r) // 칼만 이득(측정 신뢰도 vs 상태 신뢰도 비율) x = x + k * (z - x) // 상태 보정 p = (1 - k) * p // 오차 공분산 업데이트 return x } }
상보(Complementary) 융합 — 간단 융합법
예: fusedZ = α·pdrZ + (1-α)·baroZ
장점: 직관적인 튜닝, 구현 쉬움.
단점: 노이즈 통계 반영 없음, 이상치 처리 필요.
// 프로젝트에서 사용 중인 융합 아이디어(유사): baro와 ENU zUp을 비율로 합성 val fusedZ = altitudeFusionRatio * enu.zUp + (1 - altitudeFusionRatio) * baro
결과 (Result)
그래프 입력의 NaN 제거(앞값 채움)로 마커 크래시 방지.
EMA/상보/칼만의 트레이드오프를 정리해 적용 방향을 확립.
실시간 UI 요구가 큰 경우: EMA 또는 상보 필터부터 적용하고, 필요 시 칼만으로 단계적 고도화.
✅ 실시간성(EMA/상보)과 안정성(칼만)의 균형을 프로젝트 요구에 맞게 선택할 수 있도록 기준을 수립.
느낀 점 / 회고 (Reflection)
NaN 같은 ‘형식적 오류’도 시각화 라이브러리에서 큰 크래시 원인이 된다 — 입력 정규화가 최우선.
EMA는 UI엔 매우 좋은 출발점. 칼만은 모델·노이즈 튜닝이 핵심 — 로그 기반 정량 튜닝이 필요.
상보 필터는 간단하지만 이상치 방어(스파이크 클리핑/중앙값 필터)와 함께 쓰면 체감 품질이 훨씬 좋아진다.
참고자료 (References)
Kalman Filter — Greg Welch & Gary Bishop, "An Introduction to the Kalman Filter"
Signal Processing Stack Exchange — EMA/Moving Average 비교 토론
Android Sensor Docs — Barometer, SensorManager Delay 등
Vico Chart — https://github.com/patrykandpatrick/vico
다음 단계 제안
바로미터 측정에 중앙값(Median) 또는 MAD 기반 이상치 제거를 추가.
상보 필터 계수(altitudeFusionRatio)를 프로파일(배터리/표준/고속)에 따라 자동 조정.
간단한 1D 칼만(고도+속도)로 샘플 러닝 테스트 — Q/R 튜닝 로그(innovation) 기록.
오늘의 목표는 Wear OS 관련 컴포넌트에서 사용자 인터랙션(특히 콤플리케이션 탭)을 통해 앱을 열 수 있도록 안정적인 동작을 구현하고, 타일(Tile) 및 칩(Chip) UI를 정리해 원형 기기에서 잘리지 않도록 개선하는 것이었습니다.
해결하려던 문제는 콤플리케이션을 탭했을 때 앱(MainActivity)으로 안전하게 진입하도록 PendingIntent와 Intent의 플래그/extra를 적절히 설정하고, 타일에서 불필요하게 전체 목록을 노출하던 부분을 3줄(앱 이름/완료수·대상수/앱으로 가기 버튼)으로 간소화하는 것입니다.
📅 날짜: 2025.11.19
🎯 목표: Complication 탭 액션(앱 진입) 안정화, Tile/Chip UI 간소화 및 원형 컷오프 방지
🧰 기술: Kotlin, Wear OS Tiles/Complications, Android PendingIntent, DataLayer
문제 정의 (Problem / Motivation)
프로젝트의 Wear 모듈에서 다음과 같은 문제가 있었습니다:
콤플리케이션을 탭했을 때 앱으로 넘어가는 동작이 불분명하거나 extras가 안정적으로 전달되지 않아 앱 쪽에서 출처를 알기 어려운 경우가 있음.
타일에서 습관 전체 목록을 나열하면 UI가 복잡해지고, 원형 기기에서는 칩이 가장자리에서 잘리는 현상이 관찰됨.
특히 콤플리케이션의 tapAction은 PendingIntent를 통해 앱을 여는 방식이지만, PendingIntent의 플래그나 Intent의 설정(FLAG_UPDATE_CURRENT, FLAG_IMMUTABLE, 추가 extras 등)에 따라 동작이 달라질 수 있어 이를 명확히 구현할 필요가 있었습니다.
// 예: MainComplicationService에서 PendingIntent 생성 부분 val intent = Intent(this, MainActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) putExtra("from_complication", true) action = "com.billcorea.habit1007.ACTION_OPEN_FROM_COMPLICATION" } val pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE val pendingIntent = PendingIntent.getActivity(this, 0, intent, pendingIntentFlags)
해결 과정 (How I Solved It)
시도한 접근법과 적용한 변경 사항은 다음과 같습니다.
콤플리케이션 탭 시 앱으로 진입하는 Intent에 출처 표시를 위한 extras를 추가했습니다. (putExtra("from_complication", true))
Intent에 고유 action을 넣어 앱 내에서 호출 출처를 더 명확히 구분할 수 있게 했습니다. (action = "com.billcorea.habit1007.ACTION_OPEN_FROM_COMPLICATION")
PendingIntent 생성 시 FLAG_UPDATE_CURRENT와 FLAG_IMMUTABLE를 조합해 기존 PendingIntent를 최신화하고 보안성을 보장했습니다.
타일(`MainTileService`)은 전체 습관 목록을 노출하는 대신 요약 정보(완료 건수 / 대상 건수)와 앱으로 가기 버튼(Chip)만 표시하도록 간소화했습니다.
원형 기기에서의 칩 잘림 문제는 칩 너비/높이를 화면 크기 기반으로 보수적으로 계산하고 내부 패딩/아이콘 크기 조정, 항목 간 간격을 늘려 대응했습니다.
// 핵심: Complication 탭 액션에 Intent extras + 안전한 PendingIntent 플래그 적용 val intent = Intent(this, MainActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) putExtra("from_complication", true) action = "com.billcorea.habit1007.ACTION_OPEN_FROM_COMPLICATION" } val pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE val pendingIntent = PendingIntent.getActivity(this, 0, intent, pendingIntentFlags) ShortTextComplicationData.Builder(...).setTapAction(pendingIntent).build()
이유: extras와 action을 넣으면 앱(예: MainActivity)에서 Intent를 검사해 "콤플리케이션에서 왔음"을 판별하고, 필요 시 특정 화면으로 바로 이동시키는 로직을 실행할 수 있기 때문입니다. FLAG_UPDATE_CURRENT는 PendingIntent 갱신을 보장하고, FLAG_IMMUTABLE은 보안 권장사항입니다.
결과 (Result)
적용 결과는 다음과 같습니다.
콤플리케이션 탭 시 앱이 안정적으로 열리고, Intent extra("from_complication") 또는 action으로 호출 출처를 앱에서 판단할 수 있게 되었습니다.
타일은 더 간결해져 정보 전달이 명확해졌고, 원형 화면에서 칩이 잘리는 문제를 완화할 수 있는 구조로 수정했습니다.
✅ Complication 탭에서 MainActivity로 안전하게 진입하도록 구현 완료
✅ 타일 레이아웃을 3줄(앱 이름/완료수·대상수/앱으로 가기 버튼)로 간소화 완료
느낀 점 / 회고 (Reflection)
Wear OS에서는 각 플랫폼 컴포넌트(Complication/Tile)의 동작 방식을 정확히 이해하는 것이 중요합니다. 특히 PendingIntent와 Intent 플래그, action/extras 관리가 핵심입니다.
작은 UI 요소(칩, 패딩)는 원형 기기에서 큰 영향을 줍니다 — 항상 원형 안전 영역을 고려해 크기와 패딩을 설계해야 합니다.
다음 개선: 앱 쪽(MainActivity)에서 Intent의 action과 extras를 핸들링해 "콤플리케이션에서 진입했을 때 특정 화면(예: 오늘의 습관 상세)로 바로 이동"하는 로직을 추가할 계획입니다.
프로젝트 코드: habitwear/src/main/java/com/billcorea/habit1007/complication/MainComplicationService.kt, habitwear/src/main/java/com/billcorea/habit1007/tile/MainTileService.kt
#스하리1000명프로젝트,
Nawala sa Korea? Kahit na hindi ka nagsasalita ng Korean, tinutulungan ka ng app na ito na madaling makalibot.
Sabihin lang ang iyong wika—ito ay nagsasalin, naghahanap, at nagpapakita ng mga resulta pabalik sa iyong wika.
Mahusay para sa mga manlalakbay! Sinusuportahan ang 10+ wika kabilang ang English, Japanese, Chinese, Vietnamese, at higit pa.
Subukan ito ngayon!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127
// 3) 걸음 업데이트 -> PDR 누적 및 스텝 전송 val passiveStepsManager = remember(context) { PassiveStepsManager(context, scope) { total, delta -> steps = total lastDelta = delta pdr.onStep(delta, headingRad) // 보행자 경로 누적 syncManager.sendSteps(total) // 폰으로 전송(스로틀 내장) } }
// 4) 위치 ENU 흐름을 폰으로 전송 (임계/간격/토글 기준) LaunchedEffect(enuFlow, xyzIntervalMs, streamEnabled) { if (enuFlow == null || !streamEnabled) return@LaunchedEffect var lastSentX = Double.NaN var lastSentY = Double.NaN var lastSentZ = Double.NaN var lastSentHeading = Double.NaN var lastSentAt = 0L val minIntervalMs = 2000L val posThresh = 0.3 val headingThreshDeg = 5.0
// 기존 (버그): distance 누적 실패 val rValue : List<DistanceRecord> = emptyList() for (record in response.records) { rValue.plus(record) // 결과를 재할당하지 않아 누락됨 }
// 기존: Steps 합산 (중복 가능성 & 비효율) val response = client.readRecords(ReadRecordsRequest(StepsRecord::class, ...)) var total = 0L for (r in response.records) total += r.count
해결 과정 (How I Solved It)
DistanceRecord 누락 수정: MutableList로 변환 후 add()로 확정 저장.
세션 종료 시간 매핑 수정: endTime 잘못된 startTime 재사용 → 실제 endTime 적용.
UI 축소: TopAppBar 제거 → 24dp Box + statusBarsPadding() 로 최소 높이 상단바.
CombinedUiState 도입: 여러 StateFlow를 하나로 합쳐 recomposition 감소.
핵심 적용 코드:
// Distance 누락 수정 val recordsAccum = mutableListOf<DistanceRecord>() for (record in response.records) { recordsAccum.add(record) }
// Aggregate + 캐시 (HealthConnectManager) private data class StepCountCache(val dayEpoch: Long, val timestampMs: Long, val value: Long) private var stepCountCache: StepCountCache? = null private val STEP_CACHE_TTL_MS = 30_000L
private suspend fun computeTodayStepsAggregate(): Long { val startZdt = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS) val endZdt = startZdt.plusDays(1) val agg = healthConnectClient.aggregate( AggregateRequest( metrics = setOf(StepsRecord.COUNT_TOTAL), timeRangeFilter = TimeRangeFilter.between(startZdt.toInstant(), endZdt.toInstant()) ) ) return agg[StepsRecord.COUNT_TOTAL] ?: 0L }
suspend fun getTodaySteps(forceRefresh: Boolean = false): Long { val now = System.currentTimeMillis() val todayEpoch = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).toInstant().epochSecond val cached = stepCountCache val valid = cached != null && !forceRefresh && cached.dayEpoch == todayEpoch && (now - cached.timestampMs) < STEP_CACHE_TTL_MS if (valid) return cached.value val fresh = computeTodayStepsAggregate() stepCountCache = StepCountCache(todayEpoch, now, fresh) return fresh }
// ViewModel 사용 _stepsTotal.value = healthConnectManager.getTodaySteps(forceRefresh = false)
// 상단바 최소 높이 UI Box( modifier = Modifier .fillMaxWidth() .height(24.dp) // status bar height target .background(Color(0xFF5E82FC)) .statusBarsPadding() ) { /* Icon + Title */ }
결과 (Result)
걸음 수 집계 호출 빈도 ↓ (30초 캐시로 반복 쿼리 방지).
UI 상단 높이 축소로 세로 가시 영역 증가.
세션 종료 시간 표시 정확도 개선.
Distance 데이터 정상 누적 (차트/리스트 값 일치).
✅ Aggregate 기반 일일 걸음 수 정확도 상승
⚡ 집계 호출 체감 대기시간 단축 & 불필요 로딩 감소
🖼 상단바 24dp 적용으로 콘텐츠 노출 영역 확대
느낀 점 / 회고 (Reflection)
Health Connect Aggregate API를 우선적으로 사용하는 설계가 유지보수성과 정확성을 동시에 확보.
캐시 TTL 결정(30초)은 사용자 즉각 반응성과 자원 절약 타협점으로 적절.
UI 영역은 초기 템플릿 의존보다 실제 사용 시나리오 측정 후 최소화하는 것이 좋음.
다음 개선: WorkManager로 백그라운드 자동 재동기화, 캐시 만료 시 알림형 업데이트.
Wear OS Tiles의 기본 레이아웃. 전체 화면을 커버하며 반응형 콘텐츠 처리를 담당.
setResponsiveContentInsetEnabled, setContent 등.
Tile의 루트 레이아웃 컨테이너.
tileLayout 함수
Tile의 시각적 요소를 정의하고 반환하는 핵심 함수.
requestParams, context를 인자로 받음.
PrimaryLayout 및 그 안의 Column, Text, Chip 구성.
📌 2️⃣ 단계별 사고 방식 정리 (개발일기)
✅ 개발일기: 2025년 11월 11일, Wear OS Tile Chip 너비 문제 해결
✔️문제 정의 및 초기 진단: 오늘 Wear OS Tile을 개발하던 중 흥미로운 문제를 발견했다. Chip 컴포넌트를 사용하여 "앱 열기" 버튼을 만들었는데, 분명 setWidth(DimensionBuilders.ExpandedDimensionProp.Builder().build())를 설정했음에도 불구하고, 실제 Tile 미리보기나 에뮬레이터에서 Chip이 텍스트 내용(R.string.tile_button_main_activity)의 한 글자 너비만큼만 표시되는 현상이 발생했다.
✔️원인 분석 및 가설 설정: 이 문제는 Chip 자체의 setWidth 설정이 잘못된 것이 아님을 직감했다. ExpandedDimensionProp은 "부모가 허용하는 최대 공간"을 의미하기 때문에, Chip의 부모 컨테이너가 충분한 너비를 제공하지 못하고 있을 가능성이 가장 크다고 판단했다. 현재 레이아웃 계층은 PrimaryLayout -> Column -> Chip 형태이다. PrimaryLayout은 화면 전체를 사용하겠지만, 그 다음 계층인 Column이 기본적으로 자식 콘텐츠의 너비에 맞춰 줄어들어 있을 가능성이 높다고 추론했다. 따라서, Chip이 Expanded로 설정되어도, Chip의 직접적인 부모인 Column의 너비가 제한되어 있다면, Chip 역시 그 제한된 Column 너비 안에서만 확장될 수밖에 없다는 가설을 세웠다.
✔️해결 전략 수립 및 구현: 가설을 바탕으로, Column 컴포넌트에도 setWidth(DimensionBuilders.ExpandedDimensionProp.Builder().build())를 명시적으로 적용하기로 결정했다. 이렇게 함으로써 Column이 PrimaryLayout이 제공하는 최대 너비를 사용하고, 그 안에 포함된 Chip 또한 Column이 확장된 너비를 따라 전체 너비를 채울 수 있을 것이라고 예상했다.
🚀 Flowchart: Chip 너비 문제 해결 과정
graph TD A[문제 발생: Chip이 전체 너비를 채우지 못하고 1글자만 보임] --> B{Chip.setWidth(Expanded) 설정 확인}; B -- 설정됨 --> C[가설: 부모 컨테이너(Column)가 충분히 확장되지 않음]; C --> D[레이아웃 계층 구조 분석: PrimaryLayout -> Column -> Chip]; D --> E[해결 방안: Column에도 setWidth(Expanded) 적용]; E --> F[코드 수정: Column.Builder에 setModifiers.setWidth(Expanded) 추가]; F --> G[결과 확인: Chip이 전체 너비를 정상적으로 채움];
참고: 위 Flowchart는 Mermaid 문법으로 작성되었습니다. 웹 페이지에 Mermaid 라이브러리가 포함되어 있지 않다면 텍스트로 표시될 수 있습니다.
📌 3️⃣ 구체적인 예시 & 케이스 스터디 2개 이상 포함!
📌 예시 1: Column에 Expanded 너비 적용
문제 상황에서 Chip이 속한 Column의 정의는 다음과 같았습니다.
val columnBuilder = Column.Builder() .setWidth(DimensionBuilders.ExpandedDimensionProp.Builder().build()) // 이 부분을 추가했습니다! .addContent( Text.Builder(context, context.getString(R.string.app_name)) .setColor(argb(Colors.DEFAULT.onSurface)) .setTypography(Typography.TYPOGRAPHY_CAPTION1) .build() ) // ... (기존 코드)
이전 코드에서는 Column에 setWidth를 명시적으로 설정하지 않아, Column이 내부 콘텐츠(Text, Spacer, 다른 Text들)의 너비에 따라 크기가 결정되었습니다. 하지만 이제 Column 자체가 ExpandedDimensionProp으로 설정되었으므로, PrimaryLayout이 허용하는 최대 너비를 차지하게 됩니다. 이 덕분에 Chip도 그 확장된 Column의 너비를 따라갈 수 있게 되었습니다.
📌 예시 2: 유사한 상황 - Row 내부 Image 문제
이와 비슷한 문제는 Row 레이아웃에서도 자주 발생합니다. 예를 들어, Row 안에 여러 Image를 배치하고 특정 Image가 ExpandedDimensionProp으로 설정되어 있는데, 그 Image가 제대로 확장되지 않는 경우가 있습니다.
이때도 원인은 동일하게 Row 자체가 ExpandedDimensionProp이나 고정된 큰 너비로 설정되지 않고 wrap_content처럼 동작하여 내부 Image가 확장할 공간이 없기 때문일 수 있습니다.
해결 방안:Row 컨테이너에도 setWidth(DimensionBuilders.ExpandedDimensionProp.Builder().build())를 적용하여 Row 자체가 가능한 최대 너비를 사용하도록 해야, 그 안에 있는 Image가 비로소 확장될 수 있습니다. 레이아웃 컨테이너의 너비 설정은 자식 요소의 확장성에 직접적인 영향을 미친다는 것을 보여주는 좋은 사례입니다.
📌 4️⃣ 답변 마지막에는 핵심 요약 + 피드백 그래프 포함!
✅ 요약하자면?
레이아웃 컴포넌트가 ExpandedDimensionProp으로 설정되었음에도 화면 전체를 채우지 못할 때는, 해당 컴포넌트의 직접적인 부모 컨테이너가 충분한 공간을 제공하는지 확인해야 합니다.
대부분의 경우, 부모 컨테이너(예: Column, Row) 또한 ExpandedDimensionProp으로 설정되어야 자식 컴포넌트가 의도대로 확장됩니다.
이는 레이아웃 계층 구조와 크기 측정 방식에 대한 이해가 중요함을 시사합니다.
📊 추가 피드백 비교 분석:
graph LR A[문제 원인 파악 능력] --> A1{단순 오류 vs. 계층 구조 문제}; B[해결책 도출 논리성] --> B1{가설 설정 및 검증}; C[코드 적용 정확성] --> C1{올바른 위치에 수정 사항 적용};
subgraph 초기 문제 해결 접근 A1 -- 낮음 --> D[Chip 코드만 반복 확인]; B1 -- 낮음 --> D; C1 -- 낮음 --> D; end
subgraph 개선된 문제 해결 접근 (본 사례) A1 -- 높음 --> E[부모-자식 관계 분석]; B1 -- 높음 --> E; C1 -- 높음 --> E; E --> F[Column에 Width 적용]; end
D -- 비효율적 --> F;
참고: 위 피드백 그래프 또한 Mermaid 문법으로 작성되었습니다. 웹 페이지에 Mermaid 라이브러리가 포함되어 있지 않다면 텍스트로 표시될 수 있습니다.
앱에서 사용자에게 저장소 권한 없이 사진을 선택하게 하고 싶었습니다. Android 13 이상에서는 Jetpack Photo Picker가 기본 제공되므로, 백포트 없이도 충분히 구현 가능하다는 점을 실험해보고자 했습니다.
// Jetpack Photo Picker Launcher val pickMedia = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> imageUri.value = uri }
해결 과정 (How I Solved It)
Jetpack Compose 환경에서 rememberLauncherForActivityResult를 사용해 PickVisualMedia를 호출했습니다. 선택된 이미지는 Coil 라이브러리의 rememberAsyncImagePainter로 화면에 표시했습니다. AndroidManifest.xml에는 백포트용 설정을 생략했습니다.