2026/05/04

오늘의 이야기

 



🩺 Android | Health Connect 걸음 수 집계 캐시 & 상단바 최소 높이 적용


앱 이미지



 


개요 (Intro)



  • 오늘의 목표: 만보계 핵심 로직(걸음 수 집계/캐시) 안정화 + 메인 화면 상단바 UI 컴팩트화

  • 배경: 기존 raw StepsRecord 합산 방식은 성능/정확도 측면 한계. TopAppBar 기본 높이 과도.

  • 해결하려는 문제: 중복 데이터 합산 리스크, 빈번한 집계 호출로 인한 UI 지연, 화면 상단 낭비 공간

  • 사용 기술: Kotlin, Jetpack Compose, Health Connect, MVVM, Coroutine, Flow


📅 날짜: 2025.11.18
🎯 목표: Health Connect 걸음 수 Aggregate + 캐시 적용 & 상단바 높이 24dp로 축소
🧰 기술: Kotlin, Android Studio, Compose, Health Connect, MVVM, Coroutines, Flow



문제 정의 (Problem / Motivation)



  • 걸음 수 계산을 raw StepsRecord 반복 합산 → 중복/성능 저하 가능성.

  • 집계를 화면 갱신마다 수행 → 불필요한 I/O 증가.

  • TopAppBar 기본 높이로 인해 실제 콘텐츠 표시 영역 감소.

  • DistanceRecord 읽기 함수에서 plus() 결과 미반영 버그 존재(기존 리스트 누적 실패).


발견된 버그 및 개선 필요 코드:


// 기존 (버그): 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)



  1. DistanceRecord 누락 수정: MutableList로 변환 후 add()로 확정 저장.

  2. Aggregate API 도입: StepsRecord.COUNT_TOTAL 메트릭 사용.

  3. 캐시 레이어 추가: 30초 TTL + 자정 경계 무효화. getTodaySteps(forceRefresh) 제공.

  4. 세션 종료 시간 매핑 수정: endTime 잘못된 startTime 재사용 → 실제 endTime 적용.

  5. UI 축소: TopAppBar 제거 → 24dp Box + statusBarsPadding() 로 최소 높이 상단바.

  6. 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로 백그라운드 자동 재동기화, 캐시 만료 시 알림형 업데이트.




참고자료 (References)





다음 로그 예정: 백그라운드 differential changes token 주기 처리 + Room 캐시 Layer 도입.





오늘의 이야기

 



🛠 Android | Coupang API + Hilt DI + AdsScreen UX/포맷 개선 작업 기록


Ktor API 연동 페이지 샘플



개요 (Intro)



  • Ktor 기반 Coupang Affiliate API 통합, Hilt DI 구조 확립, AdsScreen UI/가격 포맷 개선

  • 네트워크 권한 및 키 로딩 안정화, 로깅/리트라이 및 브라우저 딥링크 UX 추가


📅 날짜: 2025.11.25
🎯 목표: 안정적인 외부 API 호출 + Hilt 의존성 주입 + 상품 리스트 화면 UX 향상
🧰 기술: Kotlin, Jetpack Compose, Ktor, Hilt, Coil, Gradle Kotlin DSL

문제 정의 (Problem / Motivation)



  • API 키가 빈 문자열(Empty Key)로 주입되는 문제 → local.properties 및 Gradle providers 처리 필요

  • INTERNET 권한 누락으로 OkHttp DNS SecurityException 발생

  • 가격 포맷 단순/정수 처리로 소수점 금액 누락

  • 이미지 클릭 가능성 낮음 → 링크 이동 인지 UX 부족

  • 랜덤 카테고리/재시도 로직 필요 및 불안정한 일회성 오류 대응


SecurityException: Permission denied (missing INTERNET permission?)
[CoupangKeys@Gradle] access.len=0, secret.len=0 (초기 진단 시)

해결 과정 (How I Solved It)



  • DI Hilt 모듈(NetworkModule, AppModule) 추가 → HttpClient(Ktor) & CoupangAffiliateClient 주입

  • Network INTERNET / ACCESS_NETWORK_STATE 권한 Manifest에 선언

  • Keys Gradle providers + local.properties 폴백, 길이 로그 출력 & 마스킹

  • Logging HMAC 서명 입력/시그니처 앞 8자리, 응답 status 로그

  • Retry Ktor 호출 3회 백오프 재시도 로직(ViewModel)

  • Enum 카테고리 enum화 + 랜덤 선택 기능 추가(Category.random())

  • UI AdsScreen 분리, 이미지/가격 Row 모두 브라우저 딥링크 클릭 처리

  • Format 가격 소수점 & 천단위 포맷: DecimalFormat("#,##0.########원")

  • UX OpenInNew 아이콘 + 안내 문구 후 Row 간소화(아이콘+가격) + 전체 클릭


// Hilt Module (AppModule) 일부
@Provides @Singleton
fun provideCoupangAffiliateClient(ctx: Context, http: HttpClient) = CoupangAffiliateClient(
accessKey = ctx.getString(R.string.cupang_access_key).trim(),
secretKey = ctx.getString(R.string.cupang_secret_key).trim(),
httpClient = http
)

// Retry 로직
while (attempt < maxRetries) {
try { /* API 호출 */ break } catch (e: Exception) { delay(300L * attempt++) }
}

// 가격 포맷
private fun formatPrice(raw: String?): String = DecimalFormat("#,##0.########")
.format(raw?.replace(Regex("[^0-9.]"), "")?.toBigDecimalOrNull() ?: return "-") + "원"

결과 (Result)



  • API 키 정상 주입(Gradle 로그: access.len=36, secret.len=40)

  • 네트워크 호출 정상 응답 (SecurityException 해소)

  • 상품 리스트: 이미지/가격 Row 모두 딥링크 동작, 가격 소수 유지

  • 랜덤 카테고리 새로고침으로 테스트 다양화, 오류 시 재시도 안정성 향상

  • 로그로 진단 용이(HMAC, status, key 길이)


✅ 키 주입/권한/로그 안정화 완료
🔄 재시도 + 랜덤 카테고리로 API 안정성 개선
🖼 UX: 클릭 영역 확대(이미지 + 가격 Row), 명확한 브라우저 이동 인지
💰 가격 포맷: 소수점 유지 + 천단위 표시

느낀 점 / 회고 (Reflection)



  • 권한/리소스/Gradle 구성 로그를 초기에 넣어두면 진단 속도가 크게 향상됨

  • 백오프 재시도는 간단하지만 API 레이트/에러코드 기반 세밀화 여지 있음

  • UI 클릭 affordance(아이콘, 텍스트 최소화)로 사용자 행동 유도 효과 확인

  • 가격 포맷은 BigDecimal 변환 단계에서 예외를 미리 필터링해 안정성 확보 중요


참고자료 (References)






오늘의 이야기

wear 앱 화면




🚀 개발일기: Wear OS Tile Chip 너비 문제 해결


📌 1️⃣ 핵심 개념 정리























































핵심 개념 설명 추가 정보 예시
Wear OS Tile Wear OS 기기에서 빠르고 간결한 정보 제공 및 앱 실행을 위한 사용자 인터페이스 요소. SuspendingTileService를 통해 데이터 및 UI 제공. 날씨 타일, 피트니스 통계 타일.
Chip (ProtoLayout Material) Wear OS Tiles에서 사용되는 버튼 형태의 Material 컴포넌트. 짧은 텍스트와 액션을 포함. setPrimaryLabelContent, setWidth, setChipColors 등. "앱 열기", "다음 노래" 버튼.
Column (ProtoLayout) 자식 요소들을 수직으로 배치하는 레이아웃 컨테이너. addContent, setWidth, setHeight 등. 여러 텍스트 요소나 버튼을 세로로 나열할 때.
ExpandedDimensionProp 레이아웃 요소가 부모 컨테이너의 사용 가능한 공간을 최대한 채우도록 지시하는 치수 속성. DimensionBuilders의 일부. wrap_content와 반대 개념. Chip이나 ColumnwidthExpanded로 설정.
레이아웃 계층 구조 UI 요소들이 부모-자식 관계로 중첩되어 구성되는 방식. 자식은 부모의 제약을 따름. 부모의 너비가 제한되면 자식도 제한됨. PrimaryLayout > Column > Chip 순서.
PrimaryLayout 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())를 명시적으로 적용하기로 결정했다. 이렇게 함으로써 ColumnPrimaryLayout이 제공하는 최대 너비를 사용하고, 그 안에 포함된 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: ColumnExpanded 너비 적용


문제 상황에서 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()
)
// ... (기존 코드)


이전 코드에서는 ColumnsetWidth를 명시적으로 설정하지 않아, Column이 내부 콘텐츠(Text, Spacer, 다른 Text들)의 너비에 따라 크기가 결정되었습니다. 하지만 이제 Column 자체가 ExpandedDimensionProp으로 설정되었으므로, PrimaryLayout이 허용하는 최대 너비를 차지하게 됩니다. 이 덕분에 Chip도 그 확장된 Column의 너비를 따라갈 수 있게 되었습니다.


📌 예시 2: 유사한 상황 - Row 내부 Image 문제


이와 비슷한 문제는 Row 레이아웃에서도 자주 발생합니다. 예를 들어, Row 안에 여러 Image를 배치하고 특정 ImageExpandedDimensionProp으로 설정되어 있는데, 그 Image가 제대로 확장되지 않는 경우가 있습니다.


이때도 원인은 동일하게 Row 자체가 ExpandedDimensionProp이나 고정된 큰 너비로 설정되지 않고 wrap_content처럼 동작하여 내부 Image가 확장할 공간이 없기 때문일 수 있습니다.


해결 방안: Row 컨테이너에도 setWidth(DimensionBuilders.ExpandedDimensionProp.Builder().build())를 적용하여 Row 자체가 가능한 최대 너비를 사용하도록 해야, 그 안에 있는 Image가 비로소 확장될 수 있습니다. 레이아웃 컨테이너의 너비 설정은 자식 요소의 확장성에 직접적인 영향을 미친다는 것을 보여주는 좋은 사례입니다.


📌 4️⃣ 답변 마지막에는 핵심 요약 + 피드백 그래프 포함!



요약하자면?


레이아웃 컴포넌트가 ExpandedDimensionProp으로 설정되었음에도 화면 전체를 채우지 못할 때는, 해당 컴포넌트의 직접적인 부모 컨테이너가 충분한 공간을 제공하는지 확인해야 합니다.


대부분의 경우, 부모 컨테이너(예: Column, Row) 또한 ExpandedDimensionProp으로 설정되어야 자식 컴포넌트가 의도대로 확장됩니다.


이는 레이아웃 계층 구조와 크기 측정 방식에 대한 이해가 중요함을 시사합니다.







오늘의 이야기

📸 Android | Jetpack Compose로 Photo Picker 구현 (백포트 없이)


jetpack compose photo picker



개요 (Intro)


오늘은 Android 13 이상을 대상으로 Jetpack Photo Picker를 백포트 없이 Compose로 구현해보았습니다. Google Play 서비스의 백포트 모듈을 사용하지 않고도 최신 API만으로 충분히 구현 가능하다는 점을 확인했습니다.


📅 날짜: 2025.11.06
🎯 목표: Jetpack Photo Picker를 Compose에서 구현
🧰 기술: Kotlin, Jetpack Compose, Coil, Android 13+

문제 정의 (Problem / Motivation)


앱에서 사용자에게 저장소 권한 없이 사진을 선택하게 하고 싶었습니다. 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에는 백포트용 설정을 생략했습니다.



Button(onClick = {
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
}) {
Text("사진 선택하기")
}

결과 (Result)


Android 13 이상 기기에서 저장소 권한 없이 사진을 선택하고 화면에 표시하는 데 성공했습니다. 백포트 없이도 충분히 안정적으로 동작하며, UI 반응 속도도 매우 빠릅니다.


✅ Android 13 이상에서 Photo Picker 정상 작동
📱 저장소 권한 없이 이미지 선택 가능

느낀 점 / 회고 (Reflection)


Jetpack의 최신 API는 정말 강력합니다. 백포트 없이도 충분히 유연하게 구현할 수 있었고, Compose 환경에서도 매우 직관적이었습니다. 다음에는 다중 이미지 선택 기능도 추가해보고 싶습니다.


참고자료 (References)






오늘의 이야기

 


 


오늘의 개발일지: 웹 스크래핑 삽질에서 모듈화까지


웹 스크래핑



 


작성일: 2025년 10월 28일



🚀 오늘의 목표: Daum.net 최신 뉴스, Python으로 가져오기!


오늘의 목표는 Python을 이용해 Daum.net의 최신 주요 뉴스를 가져오는 것이었습니다. 이 기능을 구현하기 위해 requests 라이브러리로 웹페이지에 접속하고, BeautifulSoup 라이브러리로 HTML에서 원하는 정보를 추출하는 '웹 스크래핑(Web Scraping)' 기술을 사용하기로 했습니다.




🚧 1차 시도와 교훈: 웹사이트는 살아있다!


처음에는 Daum 뉴스 페이지의 HTML 구조를 예측해서 뉴스 제목을 가져오는 코드를 작성했습니다. 하지만 웹사이트의 구조는 생각보다 자주 바뀌더군요. 제가 사용했던 CSS 선택자(Selector)가 더 이상 유효하지 않아 아무런 결과도 얻지 못했습니다.


⭐ 오늘의 교훈 #1: 웹사이트는 살아있는 생물과 같습니다. 스크래핑 코드는 한 번 만들고 끝이 아니라, 대상 웹사이트의 구조 변경에 맞춰 꾸준히 유지보수해야 합니다.



🚧 2차 시도와 교훈: "외계어"의 등장 (인코딩 문제)


선택자를 바꿔가며 시도한 끝에 드디어 데이터를 가져오는 데 성공했지만, 결과는 본문 바로가기 와 같은 알아볼 수 없는 글자들 뿐이었습니다. 이는 '인코딩(Encoding)' 문제 때문이었습니다. 웹사이트는 UTF-8이라는 방식으로 한글을 표시하는데, 제 코드가 이 방식을 제대로 인식하지 못한 것이죠.


이 문제는 HTML을 파싱할 때 인코딩 방식을 utf-8로 명확하게 지정해주어 해결할 수 있었습니다.


# BeautifulSoup이 인코딩을 직접 처리하도록 response.content와 from_encoding 사용
soup = BeautifulSoup(response.content, 'html.parser', from_encoding='utf-8')

⭐ 오늘의 교훈 #2: 웹에서 한글 데이터를 다룰 때는 항상 인코딩 문제를 의심해야 합니다. 데이터를 가져온 후에는 반드시 한글이 정상적으로 보이는지 확인하고, 문제가 있다면 인코딩 방식을 명시적으로 지정해주는 것이 안전합니다.



💡 문제 해결의 결정적 열쇠: 페이지 통째로 저장하기


계속된 실패에, 저는 문제의 원인이 '웹페이지를 가져오는 것' 자체에 있는지, 아니면 '가져온 데이터에서 정보를 추출하는 것'에 있는지 확인해야 했습니다. 그래서 스크래핑한 페이지의 전체 HTML을 그대로 파일(daum_page.html)로 저장하는 전략을 사용했습니다.


결과는 대성공! 파일이 정상적으로 만들어졌고, 그 안에는 모든 웹페이지 내용이 담겨 있었습니다. 이로써 웹페이지 자체는 잘 가져오고 있으며, 단지 HTML 구조가 복잡해서 정보를 추출하는 데 어려움을 겪고 있다는 것을 명확히 알 수 있었습니다.


⭐ 오늘의 교훈 #3: 복잡한 문제에 부딪혔을 때는, 문제의 범위를 좁혀나가는 것이 중요합니다. 중간 결과를 파일로 저장하거나 화면에 출력해보는 간단한 방법만으로도 문제의 진짜 원인을 찾는 결정적인 단서가 될 수 있습니다.



✨ 기능 확장 및 모듈화: 재사용 가능한 부품 만들기


이제 안정적으로 뉴스 제목을 가져올 수 있게 되었으니, 기능을 더욱 확장했습니다.



  1. 기사 본문 수집: 뉴스 목록 페이지에서 기사 링크(URL)들만 추출한 뒤, 다시 각 링크에 접속해 제목뿐만 아니라 기사 본문 내용까지 모두 가져오도록 코드를 수정했습니다.

  2. 함수로 만들기 (모듈화): 단순히 파일에 저장하고 끝나는 스크립트가 아니라, 다른 곳에서도 이 기능을 쉽게 가져다 쓸 수 있도록 doDaumSearch()라는 함수로 만들었습니다. 이 함수는 호출될 때마다 최신 뉴스 내용을 반환(return)합니다.

  3. 다른 코드와 통합: 최종적으로, 이렇게 만든 daumSearch 모듈을 원래 작업하던 메인 파일(250731-threads-naribot-posting-kanana.py)에서 import하여 사용했습니다. AI에게 글 생성을 요청하는 프롬프트(prompt)에 doDaumSearch() 함수가 실시간으로 가져온 뉴스 내용을 동적으로 추가하여, 항상 최신 뉴스를 기반으로 새로운 콘텐츠를 만들어내도록 시스템을 완성했습니다.


⭐ 오늘의 교훈 #4: 특정 기능을 완성했다면, 그것을 재사용 가능한 함수나 모듈로 만드는 습관을 들이는 것이 좋습니다. 이는 코드의 중복을 줄이고, 전체 프로그램의 구조를 더 깔끔하고 관리하기 쉽게 만들어줍니다.



오늘의 결론


단순해 보였던 뉴스 스크래핑 작업은 생각보다 많은 "삽질"을 필요로 했습니다. 하지만 선택자, 인코딩, 디버깅 전략, 그리고 모듈화에 이르기까지, 웹 개발의 기초를 다지는 값진 경험이었습니다. 역시 개발은 부딪히고 깨지면서 배우는 것 같습니다!






오늘의 이야기

wear os



 


🚀 개발일기: Wear OS Complication 클릭 시 앱 실행하기


오늘 Wear OS Complication을 탭했을 때, 내가 만든 워치 앱이 실행되도록 하는 기능을 구현했습니다. 이 기능은 사용자가 워치 페이스에서 바로 앱으로 진입할 수 있게 해주는 중요한 인터랙션입니다.


✨ 핵심 아이디어: `PendingIntent` 사용하기


Complication을 클릭해서 앱을 실행하려면, Complication 데이터에 `PendingIntent`라는 특별한 객체를 연결해야 합니다. 이 `PendingIntent`는 사용자가 Complication을 탭했을 때 시스템이 내 앱의 특정 부분을 대신 실행해 줄 수 있도록 하는 '권한' 같은 것입니다.


🛠️ 코드 수정 방법 (`MainComplicationService.kt`)


Complication 데이터를 생성하는 MainComplicationService.kt 파일의 createComplicationData 함수를 수정해야 합니다.


1. 필요한 `import` 추가


파일 상단에 다음 두 줄을 추가해주세요:



import android.content.Intent
import android.app.PendingIntent
import com.billcorea.habit1007.MainActivity // 당신의 워치 앱 메인 Activity 경로


com.billcorea.habit1007.MainActivity는 당신의 실제 워치 앱의 MainActivity 경로로 변경해야 합니다.


2. `createComplicationData` 함수 수정


createComplicationData 함수 내에서 다음처럼 `Intent`와 `PendingIntent`를 만들고 `tapAction`에 연결합니다.



private fun createComplicationData(text: String, contentDescription: String): ComplicationData {
// 1. Complication을 탭했을 때 실행할 Intent를 만듭니다.
val intent = Intent(this, MainActivity::class.java).apply {
// 앱이 새 태스크(Task)에서 시작되도록 플래그를 추가합니다.
// Complication에서 앱을 시작할 때 일반적으로 권장됩니다.
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}

// 2. 이 Intent를 PendingIntent로 감싸서, 워치 페이스가 나중에 실행할 수 있도록 합니다.
// '0'은 요청 코드 (여러 PendingIntent를 만들 때 다르게 지정).
// FLAG_IMMUTABLE은 Android 12(API 31) 이상에서 반드시 필요합니다.
val pendingIntent = PendingIntent.getActivity(
this,
0, // 요청 코드
intent,
PendingIntent.FLAG_IMMUTABLE // Android 12 이상 필수
)

// 3. ComplicationData에 위에서 만든 PendingIntent를 tapAction으로 설정합니다.
return ShortTextComplicationData.Builder(
text = PlainComplicationText.Builder(text).build(),
contentDescription = PlainComplicationText.Builder(contentDescription).build()
)
.setTapAction(pendingIntent) // 여기에 tapAction을 추가합니다!
.build()
}


⚠️ PendingIntent.FLAG_IMMUTABLE은 Android 12 (API 31) 이상을 타겟팅할 경우 필수입니다. 이 플래그를 넣지 않으면 앱이 충돌할 수 있습니다.


✅ 이제 완성입니다!


위 코드 수정 후 앱을 빌드하고 워치에 배포하면, Complication을 탭했을 때 당신의 MainActivity가 실행될 것입니다. 매우 간단하죠?



✨ 요약하자면?


Complication 클릭 시 앱을 실행하려면, MainComplicationService에서 Intent를 사용하여 실행할 Activity를 지정하고, 이를 PendingIntent로 변환한 뒤 ComplicationDatasetTapAction()에 연결하면 됩니다.






오늘의 이야기

eclipse server cannot



 


1️⃣ 글 제목


🧩 Eclipse | jQuery UI 번들 분석과 "Server cannot be resolved" 오류 해결기 ---


2️⃣ 개요 (Intro)


- 오늘의 목표: Eclipse 환경에서 웹 프로젝트 내 `main.bundle.js` 동작 오류 분석 및 Java “Server cannot be resolved” 문제 해결 - 배경: 외부에서 받은 번들 파일을 Eclipse에 올렸을 때, 실행 및 인식 오류 발생 - 사용 기술: Java, Eclipse, jQuery UI, Jetty


📅 날짜: 2025.11.03 🎯 목표: jQuery UI 번들 구조 이해 & Eclipse 실행 오류 해결 🧰 기술: Java, Eclipse, jQuery UI, Jetty, HTML

---


3️⃣ 문제 정의 (Problem / Motivation)


- 웹 프로젝트에서 `main.bundle.js` 파일을 추가했지만 실행이 되지 않음 - Eclipse 콘솔에서 `"Server cannot be resolved to a variable"` 에러 발생 - 원인 파악을 위해 `package.json` 파일과 JS 번들 구조를 분석



// 오류 예시
Server.start();
// ❌ "Server cannot be resolved to a variable"


// package.json 일부
{
"name": "jquery-ui",
"version": "1.13.3",
"license": "MIT",
"dependencies": {
"jquery": ">=1.8.0 <4.0.0"
}
}

---


4️⃣ 해결 과정 (How I Solved It)


1️⃣ `package.json`을 분석해보니, 이 번들은 **jQuery UI (v1.13.3)** 의 빌드 결과물로 확인 2️⃣ `main.bundle.js`는 Grunt로 압축된 **배포용 파일**로, 브라우저에서만 동작 3️⃣ Eclipse에서 바로 실행할 경우 `window` 객체가 없기 때문에 오류 발생 4️⃣ HTML 내 `








오늘의 이야기


#스하리1000명프로젝트,
迷失在韓國?即使您不會說韓語,這個應用程式也可以幫助您輕鬆出行。
只需說出您的語言即可 - 它會翻譯、搜尋並以您的語言顯示結果。
非常適合旅行者!支援英語、日語、中文、越南語等10多種語言。
現在就試試吧!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/05/03

오늘의 이야기


#스하리1000명프로젝트,
Đôi khi thật khó để nói chuyện với người lao động nước ngoài phải không?
Tôi đã tạo một ứng dụng đơn giản có ích! Bạn viết bằng ngôn ngữ của bạn và những người khác nhìn thấy nó bằng ngôn ngữ của họ.
Nó tự động dịch dựa trên cài đặt.
Siêu tiện dụng để trò chuyện dễ dàng. Hãy xem khi bạn có cơ hội!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

AI 생성 이미지



 


1️⃣ 글 제목


- 🐍 Python | Raspberry Pi에서 오픈소스 LLM으로 뉴스 요약기 만들기 ---


2️⃣ 개요 (Intro)


- 오늘은 라즈베리 파이에서 오픈소스 LLM을 활용해 웹 뉴스 요약기를 만드는 프로젝트를 구상했다. - 주요 목표는 Daum 포털에서 뉴스 데이터를 수집하고, 경량 LLM을 통해 300자 이내로 요약하는 기능을 구현하는 것. - 사용한 기술 스택은 Python, BeautifulSoup, Hugging Face Transformers, Phi-3 Mini 모델.


📅 날짜: 2025.11.05 🎯 목표: Raspberry Pi에서 뉴스 요약기 구상 🧰 기술: Python, Hugging Face, BeautifulSoup, Phi-3 Mini

---


3️⃣ 문제 정의 (Problem / Motivation)


- 라즈베리 파이처럼 리소스가 제한된 환경에서 LLM을 실행하려면 경량화된 모델이 필요하다. - 웹에서 뉴스 데이터를 자동으로 수집하고, 이를 요약하는 기능을 구현하려면 크롤링과 자연어 처리 기술이 결합되어야 한다. - Daum 포털의 HTML 구조를 분석해 주요 뉴스 텍스트를 추출하는 방식으로 접근했다.



# Daum 뉴스 헤드라인 수집 예시
soup.select("a.link_txt")

---


4️⃣ 해결 과정 (How I Solved It)


- Hugging Face에서 제공하는 Phi-3 Mini 모델을 선택해 Python 코드로 불러오는 방식으로 구성했다. - BeautifulSoup을 활용해 Daum 메인 페이지에서 주요 뉴스 텍스트를 추출하고, 이를 LLM에 입력해 요약 결과를 생성했다. - 전체 흐름은 뉴스 수집 → 요약 요청 → 결과 출력으로 구성되며, 추후 Streamlit을 통해 UI도 확장 가능하다.



# 요약 처리 예시
def summarize_text(text):
prompt = f"다음 내용을 300자 이내로 요약해줘:\n{text}"
inputs = tokenizer(prompt, return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=100)
return tokenizer.decode(outputs[0], skip_special_tokens=True)

---


5️⃣ 결과 (Result)


- 뉴스 헤드라인을 수집하고, LLM을 통해 간결한 요약 결과를 생성하는 데 성공했다. - 라즈베리 파이에서도 실행 가능한 경량 모델을 활용함으로써 저전력 환경에서도 AI 기능을 구현할 수 있다는 가능성을 확인했다.


✅ Daum 뉴스 요약 기능 구현 성공 📉 리소스 사용량 최소화, 실행 속도 안정적

---


6️⃣ 느낀 점 / 회고 (Reflection)


- 오픈소스 LLM의 발전 덕분에 소형 디바이스에서도 자연어 처리 기능을 구현할 수 있다는 점이 인상 깊었다. - 다음에는 Streamlit을 활용해 웹 UI를 추가하고, 요약 결과를 저장하거나 공유할 수 있는 기능을 확장해보고 싶다. - 또한 뉴스 외에도 블로그, 기술 문서 등 다양한 텍스트에 적용해보는 실험도 흥미로울 것 같다. ---


7️⃣ 참고자료 (References)


- [Hugging Face - Phi-3 Mini 모델](https://huggingface.co/microsoft/phi-3-mini) - [BeautifulSoup 공식 문서](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) - [Daum 포털](https://www.daum.net) - [Open Source LLMs in 2025 - GeeksForGeeks](https://www.geeksforgeeks.org/artificial-intelligence/top-10-open-source-llms-in-2025) ---





오늘의 이야기

 


 


🐍 Python | PC에 흩어진 .whl 파일, 한 곳으로 모으는 자동화 스크립트 개발기


AI가 그려준 이미지



 


📅 개요 (Intro)



  • 날짜: 2025.10.26

  • 목표: 여러 프로젝트와 폴더에 흩어져 있는 .whl(휠) 파일들을 하나의 지정된 폴더로 모아주는 Python 스크립트를 개발하여 라이브러리 관리를 효율화한다.

  • 기술: Python, os 모듈, shutil 모듈


🧐 문제 정의 (Problem / Motivation)


Python으로 여러 프로젝트를 진행하다 보니 가상 환경(venv), 다운로드 폴더 등 PC 곳곳에 .whl 파일들이 쌓이기 시작했습니다. 특정 라이브러리의 구버전이 필요하거나 오프라인 환경에서 설치해야 할 때, 이 파일들을 찾아 헤매는 일이 잦아졌습니다.


수동으로 *.whl을 검색해서 일일이 옮기는 것은 너무 번거롭고, 실수로 중요한 파일을 누락할 위험도 있었습니다. 이 반복적인 정리 작업을 자동화할 필요성을 느끼게 되었습니다.


🛠️ 해결 과정 (How I Solved It)


이 문제를 해결하기 위해 Python의 내장 라이브러리만을 사용하여 간단한 스크립트를 작성하기로 했습니다.


1. 파일 시스템 순회 및 .whl 파일 검색


가장 먼저 PC의 특정 드라이브(예: C:\)부터 시작해 모든 하위 폴더를 탐색해야 했습니다. Python의 os 모듈에 포함된 os.walk() 함수가 이 작업에 안성맞춤이었습니다. 이 함수는 지정된 경로의 모든 폴더와 파일을 순회하는 제너레이터(generator)를 반환해 줍니다.


파일을 찾은 후에는 문자열의 .endswith(".whl") 메서드를 이용해 확장자가 .whl인 파일만 골라 리스트에 추가했습니다.


import os

def find_whl_files(start_path):
"""지정된 경로와 그 하위 디렉토리에서 .whl 파일을 찾습니다."""
whl_files = []
for root, dirs, files in os.walk(start_path):
for file in files:
if file.endswith(".whl"):
whl_files.append(os.path.join(root, file))
return whl_files

2. 찾은 파일들을 지정된 폴더로 이동


파일 검색이 완료되면, 이제 이 파일들을 한 곳으로 옮겨야 합니다. 파일 이동, 복사, 삭제 등 파일 시스템 관련 고급 작업을 처리하는 shutil 모듈의 shutil.move() 함수를 사용했습니다.


혹시 모를 오류(권한 문제 등)에 대비해 try-except 구문으로 각 파일 이동 작업을 감싸 안정성을 높였습니다.


import shutil

# 파일을 옮길 목적지 폴더
destination_folder = r'C:\Users\nari4\downloads'

# 목적지 폴더가 없으면 생성
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)

# 찾은 파일 리스트(all_whl_files)를 순회하며 이동
for file_path in all_whl_files:
try:
shutil.move(file_path, destination_folder)
print(f"이동 완료: {file_path}")
except Exception as e:
print(f"'{file_path}' 이동 중 오류 발생: {e}")

✨ 결과 (Result)


스크립트를 실행하자 PC에 흩어져 있던 모든 .whl 파일들이 제가 지정한 C:\Users\nari4\downloads 폴더로 깔끔하게 정리되었습니다.


개선된 점:



  • 이제 필요한 .whl 파일이 있다면 지정된 폴더만 확인하면 되므로 라이브러리 관리가 매우 편해졌습니다.

  • 불필요한 파일을 찾아 헤매거나 중복으로 다운로드하는 시간이 사라졌습니다.

  • 단순 반복 작업을 자동화하여 생산성이 향상되었습니다.


실행 결과 예시:


C:\ 드라이브에서 .whl 파일을 검색합니다...

[찾은 .whl 파일 목록]
C:\projectA\venv\downloads\some_package-1.0-py3-none-any.whl
C:\Users\nari4\Downloads\another_package-2.1-cp39-cp39-win_amd64.whl

[총 2개의 .whl 파일을 'C:\Users\nari4\downloads'로 이동합니다]
이동 완료: C:\projectA\venv\downloads\some_package-1.0-py3-none-any.whl
이동 완료: C:\Users\nari4\Downloads\another_package-2.1-cp39-cp39-win_amd64.whl

파일 이동이 완료되었습니다.

📝 느낀 점 / 회고 (Reflection)



  • 교훈: 역시 "반복적인 작업이 있다면 자동화를 고민하라"는 말이 정답이었습니다. 잠시 시간을 투자해 만든 스크립트 덕분에 앞으로의 개발 환경이 훨씬 쾌적해졌습니다.

  • 기술: os.walk()shutil.move()라는 Python의 기본적이면서도 강력한 도구의 활용법을 다시 한번 되새길 수 있었습니다.

  • 다음 목표: 이 스크립트를 좀 더 발전시켜보고 싶습니다. 예를 들어, 이동 전에 파일명이 중복되는 경우 사용자에게 덮어쓸지 물어보는 옵션을 추가하거나, 오래된 버전의 .whl 파일을 식별하여 따로 분류하는 기능을 구현해 볼 계획입니다.


📚 참고자료 (References)



  • Python os 모듈 공식 문서

  • Python shutil 모듈 공식 문서





오늘의 이야기

  ⌚ Wear OS | 센서 수명주기·권한·동기화 연결로 기본 데이터 파이프 완성 워치, 모바일   개요 (Intro) 오늘의 목표: Wear OS에서 걸음 수/헤딩/고도/위치 를 ...