Skip to content

[fix] #190 NekiActionToast 버튼 터치 불가 버그 수정#191

Merged
Ojongseok merged 4 commits intodevelopfrom
fix/#190-action-toast
Apr 15, 2026
Merged

[fix] #190 NekiActionToast 버튼 터치 불가 버그 수정#191
Ojongseok merged 4 commits intodevelopfrom
fix/#190-action-toast

Conversation

@Ojongseok
Copy link
Copy Markdown
Member

@Ojongseok Ojongseok commented Apr 14, 2026

🔗 관련 이슈

📙 작업 설명

  • NekiToast가 Android Toast를 상속하는 구조에서 WindowManager 기반으로 교체
  • 기존 Toast Window는 FLAG_NOT_TOUCHABLE이 기본 적용되어 ComposeView 내 버튼 터치가 불가능했던 문제 수정(토스트 메시지 뒤의 배경이 터치되고 있었음)
  • WindowManager.LayoutParams.TYPE_APPLICATION + FLAG_NOT_FOCUSABLE 조합으로 터치 이벤트 정상 전달
  • showActionToast 버튼 클릭 시 toast가 즉시 닫히도록 dismiss 처리 추가

🧪 테스트 내역

  • showToast(), showActionToast() 동작 확인 완료

📸 스크린샷 또는 시연 영상 (선택)

기능 미리보기
액션 토스트 선택
KakaoTalk_Video_2026-04-15-00-24-55.mp4

Summary by CodeRabbit

토스트 알림 개선

  • New Features

    • 프로그래밍 방식의 닫기 기능 추가 — 코드 호출로 토스트를 안전하게 종료 가능
    • 액션 버튼 토스트: 버튼 클릭 시 토스트가 자동으로 닫힌 후 동작 실행
  • Improvements

    • 표시 방식 변경으로 더 일관된 화면 오버레이 경험 제공
    • 토스트 표시 시간 기본값 조정으로 알림 노출 최적화

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

기존 Toast 구현을 제거하고 WindowManager 기반의 ComposeView 오버레이로 교체했으며, composable에 dismiss() 콜백을 전달하고 코루틴으로 자동 dismiss를 스케줄/취소하도록 변경했습니다. 또한 공개 API 시그니처(기본값 재정의 및 파라미터 순서 변경)가 조정되었습니다.

Changes

Cohort / File(s) Summary
NekiToast 리팩토링
core/ui/src/main/java/com/neki/android/core/ui/toast/NekiToast.kt
Toast(context) 상속 제거. WindowManager + WindowManager.LayoutParams를 사용해 ComposeView 오버레이를 추가/제거하도록 변경. composable 계약을 toast: @composable (dismiss: () -> Unit) -> Unit으로 변경. 코루틴 dismissJob으로 자동 해제(delay 2500ms/3500ms) 및 수동 dismiss 취소 로직 추가. showToast() / showActionToast() 시그니처와 기본값(기본 iconRes, duration = Toast.LENGTH_SHORT) 변경 및 onClickActionButton에서 먼저 dismiss() 호출하도록 래핑.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Caller (호출자)
  participant Neki as NekiToast
  participant WM as WindowManager
  participant CV as ComposeView
  participant CJ as Coroutine (Main)

  Caller->>Neki: showToast(...)/showActionToast(...)
  Neki->>WM: create LayoutParams, addView(ComposeView)
  WM-->>CV: ComposeView attached
  Neki->>CV: setContent { toast(dismiss) }
  Neki->>CJ: launch delay(duration) -> dismiss()
  Caller->>CV: (사용자) 버튼 클릭
  CV->>Neki: invoke onClick (wrapped: dismiss() then action)
  Neki->>WM: removeView(ComposeView), cancel dismissJob
  CJ--xNeki: (if delay expires) dismiss() -> removeView
Loading

예상 코드 리뷰 노력

🎯 3 (Moderate) | ⏱️ ~25분

🐰 창문 위에 조용히 앉아,
작고 부드럽게 메시지를 띄운다,
버튼을 누르면 먼저 안녕을 고하고,
코루틴 시계가 조용히 사라지라 명하네,
토스트는 춤추며 또 사라진다 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 명확하게 #190 이슈를 참고하며 NekiActionToast 버튼 터치 불가 버그 수정을 정확히 설명합니다.
Description check ✅ Passed PR 설명이 관련 이슈, 작업 설명, 테스트 내역, 스크린샷을 포함하여 템플릿을 충분히 따릅니다.
Linked Issues check ✅ Passed WindowManager 기반 구현으로 터치 이벤트 전달 문제를 해결하고, 버튼 클릭 시 dismiss 처리를 추가하여 #190의 요구사항을 모두 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #190의 터치 불가 버그 수정과 즉시 닫기 기능 추가라는 범위 내에서 이루어졌습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/#190-action-toast

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
core/ui/src/main/java/com/neki/android/core/ui/toast/NekiToast.kt (2)

93-98: 사용하지 않는 dismiss 파라미터를 명시적으로 무시하세요.

makeToast의 람다는 dismiss: () -> Unit 파라미터를 받지만, showToast에서는 사용하지 않습니다. 가독성을 위해 _로 명시적으로 무시하는 것이 좋습니다.

✨ 코드 명확성 개선
-        makeToast(duration = duration) {
+        makeToast(duration = duration) { _ ->
             NekiToast(
                 iconRes = iconRes,
                 text = text,
             )
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/ui/src/main/java/com/neki/android/core/ui/toast/NekiToast.kt` around
lines 93 - 98, The lambda passed to makeToast currently ignores the dismiss: ()
-> Unit parameter; update the call site so the lambda explicitly ignores that
parameter (e.g. use { _ -> ... }) to improve clarity. Locate the makeToast
invocation in NekiToast.kt (the block that builds NekiToast with iconRes and
text) and change its lambda to accept and underscore the dismiss argument so the
unused parameter is clearly intentional.

36-86: 중복 토스트 표시 방지 로직 추가를 고려하세요.

현재 구현에서는 showToast 또는 showActionToast가 연속으로 호출되면 기존 토스트를 dismiss하지 않고 새 토스트가 추가됩니다. 이로 인해 여러 토스트가 겹쳐 표시되거나 리소스 누수가 발생할 수 있습니다.

♻️ 중복 토스트 방지 로직 제안
 class NekiToast(
     private val context: Context,
 ) {
     private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+    private var currentComposeView: ComposeView? = null
+    private var currentDismissJob: Job? = null

     private fun makeToast(
         duration: Int = Toast.LENGTH_SHORT,
         toast: `@Composable` (dismiss: () -> Unit) -> Unit,
     ) {
         val activity = context as ComponentActivity
-        var dismissJob: Job? = null
-        lateinit var composeView: ComposeView
+        
+        // 기존 토스트 제거
+        currentDismissJob?.cancel()
+        currentComposeView?.let {
+            if (it.isAttachedToWindow) {
+                windowManager.removeView(it)
+            }
+        }

         fun dismiss() {
-            dismissJob?.cancel()
-            if (composeView.isAttachedToWindow) {
-                windowManager.removeView(composeView)
+            currentDismissJob?.cancel()
+            currentComposeView?.let {
+                if (it.isAttachedToWindow) {
+                    windowManager.removeView(it)
+                }
             }
+            currentComposeView = null
         }

-        composeView = ComposeView(context).apply {
+        currentComposeView = ComposeView(context).apply {
             // ... 기존 코드 ...
         }

-        windowManager.addView(composeView, params)
+        windowManager.addView(currentComposeView, params)

-        dismissJob = activity.lifecycleScope.launch(Dispatchers.Main) {
+        currentDismissJob = activity.lifecycleScope.launch(Dispatchers.Main) {
             delay(if (duration == Toast.LENGTH_SHORT) 2500L else 3500L)
             dismiss()
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/ui/src/main/java/com/neki/android/core/ui/toast/NekiToast.kt` around
lines 36 - 86, The makeToast currently always creates and adds a new ComposeView
causing overlapping toasts; modify makeToast to first check and clear any
existing toast by cancelling dismissJob and removing the previous composeView
from windowManager (or calling the existing dismiss() helper) before creating a
new ComposeView. Persist the active composeView and dismissJob as instance-level
properties (e.g., currentComposeView, currentDismissJob) or reuse the existing
composeView variable so makeToast can call dismiss() /
windowManager.removeView(composeView) and cancel the job prior to
windowManager.addView, ensuring you reference makeToast, dismiss(), composeView,
dismissJob and windowManager when implementing the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/ui/src/main/java/com/neki/android/core/ui/toast/NekiToast.kt`:
- Around line 31-34: The NekiToast class constructor currently accepts a Context
but later force-casts it to ComponentActivity in makeToast, risking
ClassCastException; change the constructor parameter type from Context to
ComponentActivity (update the constructor signature for class NekiToast and any
callers), initialize windowManager via the provided ComponentActivity (replace
context.getSystemService(...) with activity.getSystemService(...)), and remove
the forced cast inside makeToast so all uses rely on the now type-safe
ComponentActivity instance.

---

Nitpick comments:
In `@core/ui/src/main/java/com/neki/android/core/ui/toast/NekiToast.kt`:
- Around line 93-98: The lambda passed to makeToast currently ignores the
dismiss: () -> Unit parameter; update the call site so the lambda explicitly
ignores that parameter (e.g. use { _ -> ... }) to improve clarity. Locate the
makeToast invocation in NekiToast.kt (the block that builds NekiToast with
iconRes and text) and change its lambda to accept and underscore the dismiss
argument so the unused parameter is clearly intentional.
- Around line 36-86: The makeToast currently always creates and adds a new
ComposeView causing overlapping toasts; modify makeToast to first check and
clear any existing toast by cancelling dismissJob and removing the previous
composeView from windowManager (or calling the existing dismiss() helper) before
creating a new ComposeView. Persist the active composeView and dismissJob as
instance-level properties (e.g., currentComposeView, currentDismissJob) or reuse
the existing composeView variable so makeToast can call dismiss() /
windowManager.removeView(composeView) and cancel the job prior to
windowManager.addView, ensuring you reference makeToast, dismiss(), composeView,
dismissJob and windowManager when implementing the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6dfbfb03-5f57-40fd-b6a1-cd3065484f1c

📥 Commits

Reviewing files that changed from the base of the PR and between 2ab1e21 and a9d5615.

📒 Files selected for processing (1)
  • core/ui/src/main/java/com/neki/android/core/ui/toast/NekiToast.kt

Comment on lines 31 to +34
class NekiToast(
private val context: Context,
) : Toast(context) {
private fun makeText(
duration: Int = LENGTH_SHORT,
toast: @Composable () -> Unit,
) {
private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Context 타입을 ComponentActivity로 변경하여 타입 안전성을 확보하세요.

현재 생성자에서 Context를 받지만, makeToast 내부(Line 40)에서 ComponentActivity로 강제 캐스팅합니다. 만약 ApplicationContextService 등 다른 Context로 인스턴스를 생성하면 런타임에 ClassCastException이 발생합니다.

🔧 타입 안전성 개선 제안
 class NekiToast(
-    private val context: Context,
+    private val activity: ComponentActivity,
 ) {
-    private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+    private val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager

이렇게 변경하면 makeToast 내부의 캐스팅도 제거할 수 있습니다:

-        val activity = context as ComponentActivity
+        // activity는 이미 ComponentActivity 타입
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class NekiToast(
private val context: Context,
) : Toast(context) {
private fun makeText(
duration: Int = LENGTH_SHORT,
toast: @Composable () -> Unit,
) {
private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
class NekiToast(
private val activity: ComponentActivity,
) {
private val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/ui/src/main/java/com/neki/android/core/ui/toast/NekiToast.kt` around
lines 31 - 34, The NekiToast class constructor currently accepts a Context but
later force-casts it to ComponentActivity in makeToast, risking
ClassCastException; change the constructor parameter type from Context to
ComponentActivity (update the constructor signature for class NekiToast and any
callers), initialize windowManager via the provided ComponentActivity (replace
context.getSystemService(...) with activity.getSystemService(...)), and remove
the forced cast inside makeToast so all uses rely on the now type-safe
ComponentActivity instance.

Copy link
Copy Markdown
Contributor

@ikseong00 ikseong00 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다! 리뷰 확인해주시고 바로 머지하셔도 될 것 같아요

Comment thread core/ui/src/main/java/com/neki/android/core/ui/toast/NekiToast.kt
Comment thread core/ui/src/main/java/com/neki/android/core/ui/toast/NekiToast.kt
@Ojongseok Ojongseok merged commit f9ee230 into develop Apr 15, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fix] 커스텀 토스트 메시지 액션 버튼 선택되는 않는 현상

3 participants