이번 경험을 통해 WebSquare Studio와 FusionChart의 연동 방식, 데이터 구조, 시각화 옵션에 대해 깊이 이해할 수 있었습니다. 특히 displayValue와 showValues의 관계, series/columns 선언의 중요성은 꼭 기억해야 할 포인트입니다.
⌚ Android Wear & Phone 연동 디버깅 | 고도 수집, 상태 동기화, Hilt 순환 참조 정리
워치앱
개요 (Intro)
오늘은 Wear OS 앱과 폰 앱 사이에서 다음 세 가지를 중점적으로 작업했다. - Wear의 TimeText 스타일 수정 (텍스트 색상 변경) - 폰 앱 설정 화면에서 시작/중지 액션을 보냈을 때, 워치 메인 화면의 상태 표시 및 고도(기압) 목록 표시 동기화 - Hilt DI 구성에서 발생한 WearDataSaver 순환 참조 오류 해결 및 SyncModule 정리
📅 날짜: 2025.12.25
🎯 목표: 폰 ↔ 워치 측정 상태/고도 데이터 동기화 및 Hilt 순환 참조 제거
🧰 기술: Kotlin, Android, Wear OS, Jetpack Compose, Hilt, Gradle
문제 정의 (Problem / Motivation)
이번에 정리한 문제들은 크게 네 가지였다. 1. Wear TimeText 색상 변경 - Wear OS의 TimeText() 컴포저블에서 시간 텍스트 색을 바꾸고 싶었다. - 문서를 보면 timeTextStyle을 통해 스타일을 주입할 수 있으나, 기본 샘플에서는 색상 변경이 적용되지 않고 있었다. 2. 폰 설정 화면의 측정 시작/중지 상태가 워치 메인 화면에 반영되지 않음 - 폰 앱 설정 화면에서: - "위치측정시작", "워치측정시작" 버튼 클릭 시 - 워치 메인 화면의 StatusSection에 "측정시작" / "측정중지" 같은 상태가 실시간(or 가깝게) 반영되길 원했다. - 하지만 현재 구현에서는, 폰 쪽에서 상태를 바꿔도 워치 UI에 반영이 되지 않거나, 반영 타이밍이 이상했다. 3. 고도(기압) 데이터 recentAltitudes 리스트가 비어 있음 - 설정 화면에서 "워치측정시작/중지"를 누르면 고도(기압) 데이터가 수신되고 있다고 log 에서는 보였지만, - UI 쪽에서 참조하는 recentAltitudes의 size가 계속 0으로 나왔다. - 즉, 실제로는 데이터 업데이트가 되고 있는데, UI에 연결되는 리스트에 값이 반영되지 않는 문제. 4. 빌드 시 Hilt 순환 참조 오류 발생 Gradle 빌드시 아래와 같은 에러가 발생했다.
error: [Dagger/DependencyCycle] Found a dependency cycle: com.billcoreatech.health501.sync.WearDataSaver is injected at [SingletonC] SyncModule.provideWearDataSaver(…, saver) com.billcoreatech.health501.sync.WearDataSaver is injected at [SingletonC] SyncModule.provideWearDataSaver(…, saver) ...
The cycle is requested via: WearDataSaver is injected at StepCounterApplication.wearDataSaver StepCounterApplication is injected at StepCounterApplication_GeneratedInjector.injectStepCounterApplication
- WearDataSaver 를 제공하는 Hilt 모듈에서 Application 과의 순환 의존이 생긴 상태였다. - DI 구성이 꼬여 있어서 Hilt 컴파일 단계에서 막힌 상황. 간단한 예시로, Hilt 모듈이 순환 참조를 만들 때의 전형적인 패턴은 다음과 같다.
@Provides @Singleton fun provideSomething(app: MyApplication): Something { // MyApplication 이 다시 Something 을 주입받고 있다면 // Dagger 입장에서는 순환 참조가 생김 return app.something } }
해결 과정 (How I Solved It)
각 문제를 단계별로 정리했다.
1. Wear TimeText 색상 변경: timeTextStyle 사용
Wear OS Compose에서 TimeText의 텍스트 색상은 timeTextStyle 파라미터를 통해 바꿀 수 있다. 핵심 아이디어는: - TimeText에 timeTextStyle = TimeTextDefaults.timeTextStyle(color = ...) 처럼 전달하거나 - 또는 TextStyle을 직접 만들어서 넘겨주는 것. 예시 코드는 아래와 같이 작성할 수 있다.
@Composable fun SampleTimeText() { // 검정색 텍스트 스타일 정의 val blackTimeTextStyle = TimeTextDefaults.timeTextStyle( color = Color.Black // 여기서 텍스트 색상을 지정 )
TimeText( timeTextStyle = blackTimeTextStyle ) }
위와 같이 TimeTextDefaults.timeTextStyle()에 color 를 명시적으로 지정하면, 기본 테마 색 대신 우리가 원하는 색(예: 검정색)으로 시간 텍스트가 표시된다.
2. 폰 설정 화면의 측정 시작/중지 → 워치 StatusSection 동기화
폰 앱에서 버튼을 누르면 워치로 명령을 보내고, 워치 메인 화면의 상태 UI 가 이를 반영하게 만드는 흐름은 대략 다음과 같이 잡았다. 1. 폰 앱 설정 화면에서 ViewModel 통해 startWatchMeasurement(), stopWatchMeasurement() 같은 함수를 호출한다. 2. 이 함수는 WearDataSyncManager (또는 유사한 sync/transport 클래스)를 사용해서, 워치로 "시작" 또는 "중지" 메시지를 보낸다. 3. 워치 측에서는 해당 메시지를 수신하는 리시버/서비스에서 MutableStateFlow 혹은 MutableLiveData에 상태를 업데이트한다. 4. 워치 메인 화면의 StatusSection 컴포저블은 이 상태 Flow 를 collect 해서 텍스트를 바꾼다. 예시 구조는 대략 이런 식이다.
// (워치 쪽) 상태를 노출하는 ViewModel 예시
class WatchMainViewModel @Inject constructor( private val statusRepository: StatusRepository ) : ViewModel() {
// 현재 측정 상태를 나타내는 Flow (예: IDLE, RUNNING, STOPPED 등) val measurementStatus: StateFlow<MeasurementStatus> = statusRepository.measurementStatus.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = MeasurementStatus.Idle ) }
@Composable fun StatusSection(viewModel: WatchMainViewModel = hiltViewModel()) { val status by viewModel.measurementStatus.collectAsStateWithLifecycle()
val statusText = when (status) { MeasurementStatus.Idle -> "대기 중" MeasurementStatus.Started -> "측정시작" MeasurementStatus.Stopped -> "측정중지" }
Text(text = statusText) }
폰에서 "워치측정시작" 버튼을 눌렀을 때는, 워치 쪽 StatusRepository에 MeasurementStatus.Started가 반영되도록 message/data layer 를 통해 값을 전송해 주면 된다. 핵심은 "폰 액션" → "워치 ViewModel 상태" → "StatusSection UI"로 이어지는 단방향 데이터 흐름을 명확히 만든 것이다.
3. recentAltitudes 리스트 0 사이즈 문제 & UI 표시
관찰한 현상은 다음과 같았다. - 로그 상으로는 "Altitude updated" 같은 메시지가 잘 찍히고 있었음. - 하지만 UI 쪽에서 바라보는 recentAltitudes의 size는 계속 0이었다. 이 경우 주로 의심해야 할 포인트는 다음 세 가지다. 1. 데이터를 추가하는 리스트 인스턴스와, UI에서 관찰하는 리스트 인스턴스가 다른가? 2. immutable 리스트를 갱신한 뒤 새로 할당하지 않고 같은 레퍼런스를 쓰고 있는가? 3. Flow/LiveData 를 관찰하는 위치와 스레드가 올바른가? 일반적인 패턴으로, 최근 고도 10개만 관리하고 UI에 보여주려면 다음과 같이 구현할 수 있다.
// 고도 데이터 모델
data class AltitudeEntry( val timestamp: Long, // 수신 시각 (millis) val altitude: Float // 고도(또는 기압 값) )
class AltitudeRepository @Inject constructor() {
// 최근 고도 리스트를 StateFlow 로 노출 private val _recentAltitudes = MutableStateFlow<List<AltitudeEntry>>(emptyList()) val recentAltitudes: StateFlow<List<AltitudeEntry>> = _recentAltitudes
/** * 새 고도 데이터를 추가하면서, 최근 10개만 유지한다. */ fun addAltitude(altitude: Float) { val newEntry = AltitudeEntry( timestamp = System.currentTimeMillis(), altitude = altitude )
// 기존 리스트를 복사해서 새 리스트 생성 val updated = (_recentAltitudes.value + newEntry) .takeLast(10) // 최근 10개만 유지
_recentAltitudes.value = updated } }
위처럼 기존 리스트에 요소를 추가한 새 리스트를 만들고, 이를 다시 Flow 에 넣어주는 방식이면, Composable 에서 collectAsState() 시 변화가 잘 감지된다. 그리고 UI 에서는 다음과 같이 그려줄 수 있다.
@Composable fun AltitudeHistoryCard( altitudeRepository: AltitudeRepository = hiltViewModel<YourViewModel>().altitudeRepository ) { val recentAltitudes by altitudeRepository.recentAltitudes .collectAsStateWithLifecycle()
// 리스트가 비어 있으면 아무것도 그리지 않도록 요구사항을 반영 if (recentAltitudes.isEmpty()) { // 요구사항: 값이 없으면 UI에 표시하지 않음 return }
위 구조가 유지되는지 점검하면서, recentAltitudes size 가 0 인 이유를 다음 순서로 검증했다. 1. addAltitude() 가 실제로 호출되는지 로그로 확인. 2. addAltitude 안에서 _recentAltitudes.value 가 변경되는지 (디버거 또는 로그). 3. UI에서 collectAsState()로 보고 있는 recentAltitudes가 동일 인스턴스인지 (같은 Repository / 같은 ViewModel 인지 확인). 최종적으로, 데이터를 업데이트하는 쪽과 UI에서 구독하는 쪽을 같은 Repository/Flow 인스턴스로 맞추고, 리스트를 불변 리스트로 재할당하도록 수정해서 recentAltitudes size가 0이 아닌 값으로 정상적으로 올라오는 것을 확인했다.
4. Hilt 순환 참조 해결: SyncModule 정리
Hilt에서 보고해 준 순환 참조 문제는 대략 다음 구조였다. - StepCounterApplication 이 WearDataSaver 를 주입받고 있음 - WearDataSaver 를 제공하는 Hilt 모듈이 다시 Application 을 참조하거나, 그 반대의 형태로 이어지면서 순환 이 문제를 해결하기 위해 DI 구성을 단순화했다. 현재 SyncModule.kt은 다음처럼 정리되어 있다.
핵심 포인트는: - Application 자체를 의존성으로 주입받지 않고, @ApplicationContext 로 제공되는 Context 만 사용하도록 변경했다는 점. - WearDataSaver 처럼 Application 과 서로 물고 물리던 타입을 모듈에서 제거하거나, 의존성을 단방향이 되도록 재구성했다. 이렇게 구성하면 Hilt 입장에서 "Application ↔ WearDataSaver" 사이의 순환 참조를 끊을 수 있어 컴파일 에러가 사라진다. 또한 WearDataSyncManager 는 Context 만 필요하므로, SingletonComponent 범위에 안전하게 둘 수 있다.
결과 (Result)
이번 수정으로 다음과 같은 결과를 얻었다.
✅ Wear TimeText 에서 timeTextStyle을 이용해 텍스트 색상을 원하는 색(검정색)으로 적용
✅ 폰 설정 화면의 측정 시작/중지 액션이 워치 메인 화면 StatusSection에 제대로 반영
✅ 고도(기압) 데이터가 recentAltitudes 리스트로 정상 수집되고, 최근 10개만 카드 UI로 표시
✅ Hilt DI의 순환 참조(WearDataSaver 관련) 오류 제거 및 SyncModule 정리
빌드 로그에서도 더 이상 [Dagger/DependencyCycle] 에러가 발생하지 않으며, 앱이 정상적으로 빌드 및 실행되는 것을 확인했다.
느낀 점 / 회고 (Reflection)
- Wear OS UI 는 일반 Compose 와 매우 비슷하지만, TimeText 같이 플랫폼 특화 컴포저블은 스타일 지정 방법을 한번 더 문서로 확인하는 게 좋다는 걸 느꼈다. - recentAltitudes 문제처럼 "로그는 찍히는데 UI 리스트는 비어 있는" 상황은, 대체로 상태 흐름(Flow/LiveData) 설계와 불변 리스트 재할당 문제로 귀결되는 경우가 많았다. - Hilt/Dagger 의 순환 참조 에러 메시지는 처음 보면 복잡하지만, "어떤 타입이 어떤 경로로 다시 자기 자신에게 돌아오는지"를 천천히 따라가다 보면 구조적인 문제를 바로잡는 계기가 된다. - 이번 정리를 통해, 폰 ↔ 워치 간 상태 및 데이터 동기화를 조금 더 명확한 단방향 흐름으로 정리할 수 있었다는 점이 가장 큰 수확이었다.
local.properties 값을 build.gradle.kts에서 java.util.Properties로 직접 로딩 시 IDE가 Unresolved reference: util, load 등 오탐 경고를 표시.
Compose 네비게이션 구현 중, Modifier.padding(innerPadding) 관련 import 누락으로 Unresolved reference 'padding' 오류.
하단 바와 NavHost를 연결하는 구조에서 패키지 경로, 아이콘 import, 경고(예: hiltViewModel deprecate 알림) 확인 필요.
// (이슈 예시) build.gradle.kts에서 직접 Properties 로딩 시 IDE 경고 발생 가능 val localProps = java.util.Properties().apply { val file = rootProject.file("local.properties") if (file.exists()) file.inputStream().use { load(it) } // load 참조 경고/오류 표기 사례 } val cupangAccessKey = localProps.getProperty("cupangAccessKey") ?: ""
🐍 Python | Hugging Face 모델, 왜 요약을 못할까? (Base vs. Instruct 모델, 버전 충돌 해결기)
개요 (Intro)
Hugging Face 모델 로딩 시 마주쳤던 인증 오류(401, 403)를 해결한 후, 새로운 문제에 직면했다. 모델이 요약 지시를 제대로 따르지 않았고, 이를 해결하는 과정에서 예상치 못한 라이브러리 버전 충돌까지 겪었다. 오늘의 일지는 이 두 가지 문제를 해결한 과정을 상세히 기록한다.
📅 날짜: 2025.11.09
🎯 목표: Gemma 모델이 뉴스 기사를 의미 있는 한국어로 요약하도록 만들기
🧰 기술: Python, Hugging Face (transformers, torch, torchvision)
문제 정의 (Problem / Motivation)
인증 문제를 모두 해결하고 `google/gemma-2b` 모델을 성공적으로 로드했지만, 정작 중요한 요약 기능이 제대로 동작하지 않았다. 게다가 문제를 해결하려다 새로운 오류까지 발생했다.
문제 1: 모델이 지시를 따르지 않음
분명 "요약해줘"라고 요청했지만, 모델은 엉뚱한 텍스트를 생성하거나 입력한 프롬프트를 그대로 반복할 뿐, 의미 있는 요약문을 만들어내지 못했다.
문제 2: `torchvision` 버전 충돌 오류
라이브러리 버전 문제 해결을 위해 `pip install --upgrade`를 실행한 후, 이전에는 없던 새로운 오류가 발생했다. 모델 로딩 단계에서 `RuntimeError: operator torchvision::nms does not exist` 라는 메시지와 함께 프로그램이 중단되었다.
ModuleNotFoundError: Could not import module 'GemmaForCausalLM'. Are this object's requirements defined correctly? ... (Caused by) RuntimeError: operator torchvision::nms does not exist
해결 과정 (How I Solved It)
두 문제는 별개의 원인을 가지고 있었고, 하나씩 해결해 나갔다.
Base 모델 vs. Instruction-Tuned 모델의 차이 이해:
첫 번째 문제의 원인은 모델 선택에 있었다. 내가 사용한 `google/gemma-2b`는 **기반(Base) 모델**로, 다음에 올 단어를 예측할 뿐 지시를 따르도록 훈련되지 않았다. "요약"과 같은 특정 작업을 수행하려면, 지시를 따르도록 미세조정(Fine-tuning)된 **Instruction-Tuned 모델 (`google/gemma-2b-it`)**을 사용해야 했다.
# 수정 전 # model_name = "google/gemma-2b"
# 수정 후: Instruction-Tuned 모델로 변경 model_name = "google/gemma-2b-it"
또한, 모델이 생성한 전체 텍스트에서 입력 프롬프트를 제외하고 순수 요약 결과만 추출하도록 코드 로직을 개선했다.
라이브러리 버전 충돌 해결:
두 번째 문제의 원인은 `torch`와 `torchvision`의 버전 호환성이 깨졌기 때문이었다. `pip install --upgrade`가 각 라이브러리를 개별적으로 최신화하면서 발생한 문제로 추정된다. 가장 확실한 해결책은 관련 라이브러리를 완전히 삭제하고 재설치하는 것이었다.
# 1. 기존 라이브러리 완전 삭제 pip uninstall torch torchvision -y
위의 과정을 통해 모든 문제를 해결하고, 마침내 `gemma-2b-it` 모델이 정상적으로 다운로드되는 것을 확인했다.
✅ `torch`와 `torchvision` 버전 충돌 문제 해결!
✅ Base 모델 대신 Instruction-Tuned 모델(`gemma-2b-it`)을 사용하도록 코드 수정 완료!
⏳ 현재 새로운 `gemma-2b-it` 모델 다운로드 진행 중 (100% 완료되면 요약 기능 정상 동작 기대)
느낀 점 / 회고 (Reflection)
LLM을 사용할 때 **Base 모델**과 **Instruction-Tuned 모델**의 차이를 아는 것이 매우 중요하다는 것을 깨달았다. 특정 작업을 시키려면 반드시 `-it`나 `-instruct`가 붙은 모델을 써야 한다.
라이브러리 버전 문제는 언제나 복병이다. `pip install --upgrade`가 만능 해결책이 아니며, 때로는 깨끗하게 삭제 후 재설치하는 것이 더 확실한 방법이다.
오류 로그를 차근차근 읽는 것이 문제 해결의 가장 빠른 길이다. `torchvision::nms`라는 키워드를 통해 `torch`와 `torchvision`의 관계를 의심해 볼 수 있었다.
#스하리1000명프로젝트,
Bị lạc ở Hàn Quốc? Ngay cả khi bạn không nói được tiếng Hàn, ứng dụng này vẫn giúp bạn đi lại dễ dàng.
Chỉ cần nói ngôn ngữ của bạn—nó sẽ dịch, tìm kiếm và hiển thị kết quả bằng ngôn ngữ của bạn.
Tuyệt vời cho du khách! Hỗ trợ hơn 10 ngôn ngữ bao gồm tiếng Anh, tiếng Nhật, tiếng Trung, tiếng Việt, v.v.
Hãy thử nó ngay bây giờ!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127
오늘은 Oracle DB에서 여러 컬럼 값 중 최댓값을 구하는 방법과 월별 데이터를 한 행으로 변환하는 방법을 실습했다. 특히 GREATEST 함수의 동작 방식과 PIVOT 구문을 활용한 데이터 구조 변환을 다뤘다. 사용한 기술 스택: Oracle SQL, SQL Developer
📅 날짜: 2025.12.19
🎯 목표: 컬럼 값 비교와 월별 데이터 Pivot 처리
🧰 기술: Oracle SQL, GREATEST, PIVOT, CASE
문제 정의 (Problem / Motivation)
특정 행(row)의 3개 컬럼 값 중 최댓값을 구해야 하는 상황이 있었다. 단순 숫자뿐 아니라 문자(R, O, Y, G) 값이 들어 있는 경우 어떤 결과가 나오는지 확인 필요. 또한 1월부터 12월까지 월별 데이터가 여러 행으로 저장된 테이블을 한 행으로 변환해 보고 싶었다.
-- 예시: 특정 행의 3개 컬럼 중 최댓값 구하기 SELECT GREATEST(col1, col2, col3) AS max_value FROM your_table WHERE id = 123;
해결 과정 (How I Solved It)
GREATEST 함수는 숫자뿐 아니라 문자도 비교 가능하다는 점을 확인했다. 문자 비교 시 ASCII 코드 기준으로 가장 큰 값이 반환됨. (예: R=82, O=79, Y=89, G=71 → 최댓값은 Y) 의미적 순서(예: R > O > Y > G)를 반영하려면 CASE 또는 DECODE로 매핑 후 비교해야 한다.
-- 색상 우선순위 매핑 후 최댓값 구하기 SELECT CASE GREATEST( DECODE(col1, 'R',4, 'O',3, 'Y',2, 'G',1), DECODE(col2, 'R',4, 'O',3, 'Y',2, 'G',1), DECODE(col3, 'R',4, 'O',3, 'Y',2, 'G',1) ) WHEN 4 THEN 'R' WHEN 3 THEN 'O' WHEN 2 THEN 'Y' WHEN 1 THEN 'G' END AS max_color FROM your_table WHERE id = 123;
월별 데이터를 한 행으로 변환하기 위해 PIVOT을 활용했다.
-- PIVOT을 이용한 월별 데이터 변환 SELECT * FROM sales_data PIVOT ( SUM(amount) FOR month IN (1 AS JAN, 2 AS FEB, 3 AS MAR, 4 AS APR, 5 AS MAY, 6 AS JUN, 7 AS JUL, 8 AS AUG, 9 AS SEP, 10 AS OCT, 11 AS NOV, 12 AS DEC) );
결과 (Result)
GREATEST로 숫자와 문자를 비교할 수 있음을 확인했고, 문자 비교 시 ASCII 기준이라는 점을 배웠다. 색상 순서를 반영하려면 매핑이 필요하다는 교훈을 얻었다. PIVOT을 통해 월별 데이터를 한 행으로 변환하는 데 성공했다.
✅ 컬럼 값 비교 로직 정리 완료✅ 월별 데이터 Pivot 변환 성공📊 데이터 구조 이해도 향상
느낀 점 / 회고 (Reflection)
단순히 함수만 쓰는 것이 아니라, 데이터의 의미를 반영하려면 추가 로직이 필요하다는 점을 깨달았다. PIVOT은 데이터 분석 시 매우 유용하며, 앞으로 보고서 작성이나 BI 툴 연동 시 자주 활용할 수 있을 것 같다. SQL은 단순 조회를 넘어서 데이터 구조를 재구성하는 강력한 도구라는 점을 다시 느꼈다.
참고자료 (References)
Oracle SQL Functions - GREATEST Oracle PIVOT Clause 공식 문서
// 예시 (정리 이전): 단일 컴포저블 안에 다양한 UI가 혼재해 가독성 저하 @Composable fun WearApp() { // ... 상태/권한/서비스 로직 Scaffold { ScalingLazyColumn { // 권한, 상태, 위치, PDR, 배터리 섹션이 모두 item 블록으로 길게 나열됨 } } }
해결 과정 (How I Solved It)
UI를 작은 컴포저블들로 분리: PermissionSection, StatusSection, LocationSection, PdrSection
텍스트 중앙정렬: 모든 텍스트 컴포넌트에 Modifier.fillMaxWidth() + TextAlign.Center 적용
문자열 리소스화: 하드코딩된 한글("전송 임계값=")을 strings.xml로 이전
불필요한 배터리 최적화 섹션 제거
// 파일: MainActivity.kt — 초보자도 이해하기 쉬운 주석 포함 예시 // 1) 가운데 정렬을 위한 import import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.ui.text.style.TextAlign
@Composable fun StatusSection( syncStatusText: String, syncStatusColor: Color, steps: Long, lastDelta: Long, stepThreshold: Long, headingRad: Double, lastPayload: StepPayload?, altitude: Double?, unit: String ) { // Column의 수평 정렬은 Center로 유지하되, // 각 Text를 화면 너비만큼 확장하고(Text가 가로폭을 차지), // TextAlign.Center로 내용을 중앙에 배치 Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( text = syncStatusText, color = syncStatusColor, style = MaterialTheme.typography.bodySmall, modifier = Modifier.fillMaxWidth(), // 전체 너비 사용 textAlign = TextAlign.Center // 텍스트 중앙 정렬 ) Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.steps_label) + ": " + steps + " (Δ " + lastDelta + ")", style = MaterialTheme.typography.bodySmall, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center ) Text( text = stringResource(R.string.step_threshold_label) + stepThreshold, style = MaterialTheme.typography.bodySmall, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center ) Text( text = String.format( java.util.Locale.US, stringResource(R.string.heading_label) + ": %.0f°", Math.toDegrees(headingRad) ), style = MaterialTheme.typography.bodySmall, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center ) } }
전송 임계값=
// 파일: MainActivity.kt — 배터리 최적화 섹션 제거 예시 // 기존: BatteryOptimizationSection(...) 사용 및 관련 PositionIndicator 주변 조건부 렌더링 // 변경: 해당 섹션/조건 로직 삭제로 UI 단순화
결과 (Result)
UI가 섹션별 컴포저블로 분리되어 가독성 및 유지보수성 향상
텍스트 중앙정렬로 일관된 시각 경험 제공
문자열 리소스화로 국제화/재사용성 개선
불필요한 배터리 최적화 UI 제거로 사용자 혼란 감소
✅ Compose 아이템들이 깔끔하게 모듈화됨
🎯 텍스트 중앙정렬 적용으로 UI 일관성 확보
🧩 문자열 리소스 관리로 다국어 및 유지보수 용이
느낀 점 / 회고 (Reflection)
Wear Compose에서도 작은 컴포저블로 나누는 것이 가장 큰 생산성 향상을 가져온다
하드코딩 텍스트는 사소해 보여도 국제화/테스트/일관성 측면에서 리소스화가 중요
기기 특성(Wear OS)을 고려한 UI/설정 항목은 실제 동작 가능 여부를 검증 후 제공해야 한다
#billcorea #운동동아리관리앱
🏸 Schneedle, aplikasi yang wajib dimiliki oleh klub bulu tangkis!
👉 Match Play – Rekam Skor & Temukan Lawan 🎉
Sempurna untuk di mana saja, sendirian, bersama teman, atau di klub! 🤝
Jika Anda suka bulu tangkis, cobalah